From 7fecf27abfd42423311bf9684fddd7d1773ac517 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 14 Oct 2025 10:34:49 +0200 Subject: [PATCH 01/23] Remove ContributionManager and ContributionPane UI files Deleted ContributionManager.kt and ContributionPane.kt from the contrib/ui directory. This removes the Compose-based contributions manager and its detail pane prototypes which got merged unnecessarily --- .../app/contrib/ui/ContributionManager.kt | 310 ------------------ .../app/contrib/ui/ContributionPane.kt | 79 ----- 2 files changed, 389 deletions(-) delete mode 100644 app/src/processing/app/contrib/ui/ContributionManager.kt delete mode 100644 app/src/processing/app/contrib/ui/ContributionPane.kt diff --git a/app/src/processing/app/contrib/ui/ContributionManager.kt b/app/src/processing/app/contrib/ui/ContributionManager.kt deleted file mode 100644 index 2ad472159b..0000000000 --- a/app/src/processing/app/contrib/ui/ContributionManager.kt +++ /dev/null @@ -1,310 +0,0 @@ -package processing.app.contrib.ui - -import androidx.compose.animation.Animatable -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposePanel -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration -import kotlinx.serialization.Serializable -import processing.app.Platform -import processing.app.loadPreferences -import java.net.URL -import java.util.* -import javax.swing.JFrame -import javax.swing.SwingUtilities -import kotlin.io.path.* - - -fun main() = application { - Window(onCloseRequest = ::exitApplication) { - contributionsManager() - } -} - -enum class Status { - VALID, - BROKEN, - DEPRECATED -} -enum class Type { - library, - mode, - tool, - examples, -} - -@Serializable -data class Author( - val name: String, - val url: String? = null, -) - -@Serializable -data class Contribution( - val id: Int, - val status: Status, - val source: String, - val type: Type, - val name: String? = null, - val categories: List? = emptyList(), - val authors: String? = null, - val authorList: List? = emptyList(), - val url: String? = null, - val sentence: String? = null, - val paragraph: String? = null, - val version: String? = null, - val prettyVersion: String? = null, - val minRevision: Int? = null, - val maxRevision: Int? = null, - val download: String? = null, - val isUpdate: Boolean? = null, - val isInstalled: Boolean? = null, -) - -@Serializable -data class Contributions( - val contributions: List -) - -fun openContributionsManager(){ - // open the compose window - - SwingUtilities.invokeLater { - val frame = JFrame("Contributions Manager") - frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE - frame.setSize(800, 600) - - val composePanel = ComposePanel() - composePanel.setContent { - contributionsManager() - } - - frame.contentPane.add(composePanel) - frame.isVisible = true - } -} - -@Composable -fun contributionsManager(){ - var contributions by remember { mutableStateOf(listOf()) } - var localContributions by remember { mutableStateOf(listOf()) } - var error by remember { mutableStateOf(null) } - - val preferences = loadPreferences() - - LaunchedEffect(preferences){ - try { - localContributions = loadContributionProperties(preferences) - .map { (type, props) -> - Contribution( - id = 0, - status = Status.VALID, - source = "local", - type = type, - name = props.getProperty("name"), - authors = props.getProperty("authors"), - url = props.getProperty("url"), - sentence = props.getProperty("sentence"), - paragraph = props.getProperty("paragraph"), - version = props.getProperty("version"), - prettyVersion = props.getProperty("prettyVersion"), - minRevision = props.getProperty("minRevision")?.toIntOrNull(), - maxRevision = props.getProperty("maxRevision")?.toIntOrNull(), - download = props.getProperty("download"), - ) - } - } catch (e: Exception){ - error = e - } - } - - - LaunchedEffect(Unit){ - try { - val url = URL("https://github.com/mingness/processing-contributions-new/raw/refs/heads/main/contributions.yaml") - val connection = url.openConnection() - val inputStream = connection.getInputStream() - val yaml = inputStream.readAllBytes().decodeToString() - // TODO cache yaml in processing folder - - val parser = Yaml( - configuration = YamlConfiguration( - strictMode = false - ) - ) - val result = parser.decodeFromString(Contributions.serializer(), yaml) - - contributions = result.contributions - .filter { it.status == Status.VALID } - .map { - // TODO Parse better - val authorList = it.authors?.split(",")?.map { author -> - val parts = author.split("](") - val name = parts[0].removePrefix("[") - val url = parts.getOrNull(1)?.removeSuffix(")") - Author(name, url) - } ?: emptyList() - it.copy(authorList = authorList) - } - } catch (e: Exception){ - error = e - } - } - if(error != null){ - Text("Error loading contributions: ${error?.message}") - return - } - if(contributions.isEmpty()){ - Text("Loading contributions...") - return - } - - val contributionsByType = (contributions + localContributions) - .groupBy { it.name } - .map { (_, contributions) -> - if(contributions.size == 1) return@map contributions.first() - else{ - // check if they all have the same version, otherwise return the newest version - val versions = contributions.mapNotNull { it.version } - if(versions.toSet().size == 1) return@map contributions.first().copy(isInstalled = true) - else{ - val newest = contributions.maxByOrNull { it.version?.toIntOrNull() ?: 0 } - if(newest != null) return@map newest.copy(isUpdate = true, isInstalled = true) - else return@map contributions.first().copy(isUpdate = true, isInstalled = true) - } - } - } - .groupBy { it.type } - - val types = Type.entries - var selectedType by remember { mutableStateOf(types.first()) } - val contributionsForType = (contributionsByType[selectedType] ?: emptyList()) - .sortedBy { it.name } - - var selectedContribution by remember { mutableStateOf(null) } - Box{ - Column { - Row{ - for(type in types){ - val background = remember { Animatable(Color.Transparent) } - val color = remember { Animatable(Color.Black) } - LaunchedEffect(selectedType){ - if(selectedType == type){ - background.animateTo(Color(0xff0251c8)) - color.animateTo(Color.White) - }else{ - background.animateTo(Color.Transparent) - color.animateTo(Color.Black) - } - } - - Row(modifier = Modifier - .background(background.value) - .pointerHoverIcon(PointerIcon.Hand) - .clickable { - selectedType = type - selectedContribution = null - } - .padding(16.dp, 8.dp) - ){ - Text(type.name, color = color.value) - val updates = contributionsByType[type]?.count { it.isUpdate == true } ?: 0 - if(updates > 0){ - Text("($updates)") - } - } - } - } - - Box(modifier = Modifier.weight(1f)){ - val state = rememberLazyListState() - LazyColumn(state = state) { - item{ - // Table Header - } - items(contributionsForType){ contribution -> - Row(modifier = Modifier - .pointerHoverIcon(PointerIcon.Hand) - .clickable { selectedContribution = contribution } - .padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row(modifier = Modifier.weight(1f)){ - if(contribution.isUpdate == true){ - Text("Update") - }else if(contribution.isInstalled == true){ - Text("Installed") - } - - } - Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(8f)){ - Text(contribution.name ?: "Unnamed", fontWeight = FontWeight.Bold) - Text(contribution.sentence ?: "No description", maxLines = 1, overflow = TextOverflow.Ellipsis) - } - Row(modifier = Modifier.weight(4f)){ - Text(contribution.authorList?.joinToString { it.name } ?: "Unknown") - } - } - } - } - VerticalScrollbar( - modifier = Modifier - .align(Alignment.CenterEnd) - .background(Color.LightGray) - .fillMaxHeight(), - adapter = rememberScrollbarAdapter( - scrollState = state - ) - ) - } - ContributionPane( - contribution = selectedContribution, - onClose = { selectedContribution = null } - ) - } - - } - -} - - -fun loadContributionProperties(preferences: Properties): List>{ - val result = mutableListOf>() - val sketchBook = Path(preferences.getProperty("sketchbook.path.four", Platform.getDefaultSketchbookFolder().path)) - sketchBook.forEachDirectoryEntry{ contributionsFolder -> - if(!contributionsFolder.isDirectory()) return@forEachDirectoryEntry - val typeName = contributionsFolder.fileName.toString() - val type: Type = when(typeName){ - "libraries" -> Type.library - "modes" -> Type.mode - "tools" -> Type.tool - "examples" -> Type.examples - else -> return@forEachDirectoryEntry - } - contributionsFolder.forEachDirectoryEntry { contribution -> - if(!contribution.isDirectory()) return@forEachDirectoryEntry - contribution.forEachDirectoryEntry("*.properties"){ entry -> - val props = Properties() - props.load(entry.inputStream()) - result += Pair(type, props) - } - } - } - return result -} \ No newline at end of file diff --git a/app/src/processing/app/contrib/ui/ContributionPane.kt b/app/src/processing/app/contrib/ui/ContributionPane.kt deleted file mode 100644 index 2f4a96931b..0000000000 --- a/app/src/processing/app/contrib/ui/ContributionPane.kt +++ /dev/null @@ -1,79 +0,0 @@ -package processing.app.contrib.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Window - -//--processing-blue-light: #82afff; -//--processing-blue-mid: #0564ff; -//--processing-blue-deep: #1e32aa; -//--processing-blue-dark: #0f195a; -//--processing-blue: #0251c8; - -@Composable -fun ContributionPane(contribution: Contribution?, onClose: () -> Unit) { - if(contribution == null) { - return - } - val typeName = when(contribution.type) { - Type.library -> "Library" - Type.tool -> "Tool" - Type.examples -> "Example" - Type.mode -> "Mode" - } - Window( - title = "${typeName}: ${contribution.name}", - onCloseRequest = onClose, - onKeyEvent = { - if(it.key == Key.Escape) { - onClose() - true - } else { - false - } - } - ){ - Box { - Column(modifier = Modifier.padding(10.dp)) { - Text(typeName, style = TextStyle(fontSize = 16.sp)) - Text(contribution.name ?: "", style = TextStyle(fontSize = 20.sp)) - Row(modifier = Modifier.padding(0.dp, 10.dp)) { - val action = when(contribution.isUpdate) { - true -> "Update" - false, null -> when(contribution.isInstalled) { - true -> "Uninstall" - false, null -> "Install" - } - } - Text(action, - style = TextStyle(fontSize = 14.sp, color = Color.White), - modifier = Modifier - .clickable { - - } - .pointerHoverIcon(PointerIcon.Hand) - .background(Color(0xff0251c8)) - .padding(24.dp,12.dp) - ) - } - Text(contribution.paragraph ?: "", style = TextStyle(fontSize = 14.sp)) - } - } - } - -} \ No newline at end of file From 838ea14cd52a1a3d9a3779b6ce0010a22379f1ea Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 14 Oct 2025 11:53:38 +0200 Subject: [PATCH 02/23] Enhance Preferences reactivity and test coverage Refactored ReactiveProperties to use snapshotStateMap for Compose reactivity. Improved PreferencesProvider and watchFile composables with better file watching, override support via system properties, and added documentation. Updated PreferencesKtTest to use temporary files and verify file-to-UI reactivity. --- app/src/processing/app/Preferences.kt | 163 ++++++++++++++++--- app/test/processing/app/PreferencesKtTest.kt | 61 +++++++ 2 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 app/test/processing/app/PreferencesKtTest.kt diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c5645c9bbc..c54cbbd817 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -2,56 +2,183 @@ package processing.app import androidx.compose.runtime.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch import java.io.File import java.io.InputStream import java.nio.file.* import java.util.Properties +/* + The ReactiveProperties class extends the standard Java Properties class + to provide reactive capabilities using Jetpack Compose's mutableStateMapOf. + This allows UI components to automatically update when preference values change. +*/ +class ReactiveProperties: Properties() { + val snapshotStateMap = mutableStateMapOf() + + override fun setProperty(key: String, value: String) { + super.setProperty(key, value) + snapshotStateMap[key] = value + } + + override fun getProperty(key: String): String? { + return snapshotStateMap[key] ?: super.getProperty(key) + } + + operator fun get(key: String): String? = getProperty(key) + + operator fun set(key: String, value: String) { + setProperty(key, value) + } +} + +/* + A CompositionLocal to provide access to the ReactiveProperties instance + throughout the composable hierarchy. + */ +val LocalPreferences = compositionLocalOf { error("No preferences provided") } const val PREFERENCES_FILE_NAME = "preferences.txt" const val DEFAULTS_FILE_NAME = "defaults.txt" -fun PlatformStart(){ - Platform.inst ?: Platform.init() -} +/* + This composable function sets up a preferences provider that manages application settings. + It initializes the preferences from a file, watches for changes to that file, and saves + any updates back to the file. It uses a ReactiveProperties class to allow for reactive + updates in the UI when preferences change. + usage: + PreferencesProvider { + // Your app content here + } + + to access preferences: + val preferences = LocalPreferences.current + val someSetting = preferences["someKey"] ?: "defaultValue" + preferences["someKey"] = "newValue" + + This will automatically save to the preferences file and update any UI components + that are observing that key. + + to override the preferences file (for testing, etc) + System.setProperty("processing.app.preferences.file", "/path/to/your/preferences.txt") + to override the debounce time (in milliseconds) + System.setProperty("processing.app.preferences.debounce", "200") + + */ +@OptIn(FlowPreview::class) @Composable -fun loadPreferences(): Properties{ - PlatformStart() +fun PreferencesProvider(content: @Composable () -> Unit){ + val preferencesFileOverride: File? = System.getProperty("processing.app.preferences.file")?.let { File(it) } + val preferencesDebounceOverride: Long? = System.getProperty("processing.app.preferences.debounce")?.toLongOrNull() - val settingsFolder = Platform.getSettingsFolder() - val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME) + // Initialize the platform (if not already done) to ensure we have access to the settings folder + remember { + Platform.init() + } + // Grab the preferences file, creating it if it doesn't exist + // TODO: This functionality should be separated from the `Preferences` class itself + val settingsFolder = Platform.getSettingsFolder() + val preferencesFile = preferencesFileOverride ?: settingsFolder.resolve(PREFERENCES_FILE_NAME) if(!preferencesFile.exists()){ + preferencesFile.mkdirs() preferencesFile.createNewFile() } - watchFile(preferencesFile) - return Properties().apply { - load(ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: InputStream.nullInputStream()) - load(preferencesFile.inputStream()) + val update = watchFile(preferencesFile) + + + val properties = remember(preferencesFile, update) { + ReactiveProperties().apply { + val defaultsStream = ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) + ?: InputStream.nullInputStream() + load(defaultsStream + .reader(Charsets.UTF_8) + ) + load(preferencesFile + .inputStream() + .reader(Charsets.UTF_8) + ) + } + } + + val initialState = remember(properties) { properties.snapshotStateMap.toMap() } + + // Listen for changes to the preferences and save them to file + LaunchedEffect(properties) { + snapshotFlow { properties.snapshotStateMap.toMap() } + .dropWhile { it == initialState } + .debounce(preferencesDebounceOverride ?: 100) + .collect { + + // Save the preferences to file, sorted alphabetically + preferencesFile.outputStream().use { output -> + output.write( + properties.entries + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.key.toString() }) + .joinToString("\n") { (key, value) -> "$key=$value" } + .toByteArray() + ) + } + } + } + + CompositionLocalProvider(LocalPreferences provides properties){ + content() } + } +/* + This composable function watches a specified file for modifications. When the file is modified, + it updates a state variable with the latest WatchEvent. This can be useful for triggering UI updates + or other actions in response to changes in the file. + + To watch the file at the fasted speed (for testing) set the following system property: + System.setProperty("processing.app.watchfile.forced", "true") + */ @Composable fun watchFile(file: File): Any? { + val forcedWatch: Boolean = System.getProperty("processing.app.watchfile.forced").toBoolean() + val scope = rememberCoroutineScope() var event by remember(file) { mutableStateOf?> (null) } DisposableEffect(file){ val fileSystem = FileSystems.getDefault() val watcher = fileSystem.newWatchService() + var active = true + // In forced mode we just poll the last modified time of the file + // This is not efficient but works better for testing with temp files + val toWatch = { file.lastModified() } + var state = toWatch() + val path = file.toPath() val parent = path.parent val key = parent.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY) scope.launch(Dispatchers.IO) { while (active) { - for (modified in key.pollEvents()) { - if (modified.context() != path.fileName) continue - event = modified + if(forcedWatch) { + if(toWatch() == state) continue + state = toWatch() + event = object : WatchEvent { + override fun count(): Int = 1 + override fun context(): Path = file.toPath().fileName + override fun kind(): WatchEvent.Kind = StandardWatchEventKinds.ENTRY_MODIFY + override fun toString(): String = "ForcedEvent(${context()})" + } + continue + }else{ + for (modified in key.pollEvents()) { + if (modified.context() != path.fileName) continue + event = modified + } } } } @@ -62,12 +189,4 @@ fun watchFile(file: File): Any? { } } return event -} -val LocalPreferences = compositionLocalOf { error("No preferences provided") } -@Composable -fun PreferencesProvider(content: @Composable () -> Unit){ - val preferences = loadPreferences() - CompositionLocalProvider(LocalPreferences provides preferences){ - content() - } } \ No newline at end of file diff --git a/app/test/processing/app/PreferencesKtTest.kt b/app/test/processing/app/PreferencesKtTest.kt new file mode 100644 index 0000000000..6b5dbc5ea9 --- /dev/null +++ b/app/test/processing/app/PreferencesKtTest.kt @@ -0,0 +1,61 @@ +package processing.app + +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.* +import java.util.Properties +import kotlin.io.path.createFile +import kotlin.io.path.createTempDirectory +import kotlin.test.Test + +class PreferencesKtTest{ + @OptIn(ExperimentalTestApi::class) + @Test + fun testKeyReactivity() = runComposeUiTest { + val directory = createTempDirectory("preferences") + val tempPreferences = directory + .resolve("preferences.txt") + .createFile() + .toFile() + + // Set system properties for testing + System.setProperty("processing.app.preferences.file", tempPreferences.absolutePath) + System.setProperty("processing.app.preferences.debounce", "0") + System.setProperty("processing.app.watchfile.forced", "true") + + val newValue = (0..Int.MAX_VALUE).random().toString() + val testKey = "test.preferences.reactivity" + + setContent { + PreferencesProvider { + val preferences = LocalPreferences.current + Text(preferences[testKey] ?: "default", modifier = Modifier.testTag("text")) + + Button(onClick = { + preferences[testKey] = newValue + }, modifier = Modifier.testTag("button")) { + Text("Change") + } + } + } + + onNodeWithTag("text").assertTextEquals("default") + onNodeWithTag("button").performClick() + onNodeWithTag("text").assertTextEquals(newValue) + + val preferences = Properties() + preferences.load(tempPreferences.inputStream().reader(Charsets.UTF_8)) + + // Check if the preference was saved to file + assert(preferences[testKey] == newValue) + + + val nextValue = (0..Int.MAX_VALUE).random().toString() + // Overwrite the file to see if the UI updates + tempPreferences.writeText("$testKey=${nextValue}") + + onNodeWithTag("text").assertTextEquals(nextValue) + } +} \ No newline at end of file From e1e1e3a602d6791b99f4bd605f0fbda69a5610d4 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 14 Oct 2025 12:02:05 +0200 Subject: [PATCH 03/23] Small bugfix for removed function --- app/src/processing/app/ui/theme/Locale.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 254c0946c1..df37eb13a7 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -3,10 +3,9 @@ package processing.app.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf -import processing.app.LocalPreferences +import androidx.compose.runtime.remember import processing.app.Messages import processing.app.Platform -import processing.app.PlatformStart import processing.app.watchFile import java.io.File import java.io.InputStream @@ -32,7 +31,9 @@ class Locale(language: String = "") : Properties() { val LocalLocale = compositionLocalOf { Locale() } @Composable fun LocaleProvider(content: @Composable () -> Unit) { - PlatformStart() + remember { + Platform.init() + } val settingsFolder = Platform.getSettingsFolder() val languageFile = File(settingsFolder, "language.txt") From 20fa9be3b8c3cbfddca5bd736a812efde7cdac72 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 14 Oct 2025 12:49:25 +0200 Subject: [PATCH 04/23] Add compose ui test to the deps --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0d3fcbd12d..1aea9ac6b2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ import org.gradle.internal.jvm.Jvm import org.gradle.internal.os.OperatingSystem import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download @@ -119,6 +120,8 @@ dependencies { implementation(libs.markdown) implementation(libs.markdownJVM) + @OptIn(ExperimentalComposeLibrary::class) + testImplementation(compose.uiTest) testImplementation(kotlin("test")) testImplementation(libs.mockitoKotlin) testImplementation(libs.junitJupiter) From 5ae8b8440496979c9d159b719d32f0c7496463c6 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 23 Oct 2025 12:34:06 +0200 Subject: [PATCH 05/23] Refactor theme system to Material 3 color schemes Replaces legacy color definitions with Material 3 color schemes and introduces extended color support for warnings. Dialogs in Messages.kt are now implemented using Compose Material 3 components for a modern UI. Removes deprecated color sets and updates PDETheme to use new color schemes, improving consistency and maintainability. --- app/src/processing/app/Messages.kt | 173 +++++++++-- app/src/processing/app/ui/Preferences.kt | 2 - app/src/processing/app/ui/theme/Colors.kt | 46 +-- app/src/processing/app/ui/theme/Theme.kt | 132 +++++---- app/src/processing/app/ui/theme/m3/Color.kt | 248 ++++++++++++++++ app/src/processing/app/ui/theme/m3/Theme.kt | 301 ++++++++++++++++++++ 6 files changed, 784 insertions(+), 118 deletions(-) create mode 100644 app/src/processing/app/ui/theme/m3/Color.kt create mode 100644 app/src/processing/app/ui/theme/m3/Theme.kt diff --git a/app/src/processing/app/Messages.kt b/app/src/processing/app/Messages.kt index cae54e6e97..05bada362c 100644 --- a/app/src/processing/app/Messages.kt +++ b/app/src/processing/app/Messages.kt @@ -18,13 +18,41 @@ */ package processing.app +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Alarm +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeDialog +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import com.formdev.flatlaf.FlatLightLaf import processing.app.ui.Toolkit +import processing.app.ui.theme.PDETheme +import java.awt.Dimension import java.awt.EventQueue import java.awt.Frame import java.io.PrintWriter import java.io.StringWriter import javax.swing.JFrame import javax.swing.JOptionPane +import javax.swing.UIManager class Messages { companion object { @@ -37,10 +65,21 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - JOptionPane.showMessageDialog( - Frame(), message, title, - JOptionPane.INFORMATION_MESSAGE - ) + showDialog(title) { dismiss -> + AlertDialog( + onDismissRequest = { }, + icon = { Icon(Icons.Default.Info, contentDescription = "Info!") }, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + Button( + onClick = { dismiss() } + ) { + Text("OK") + } + } + ) + } } } @@ -57,10 +96,25 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - JOptionPane.showMessageDialog( - Frame(), message, title, - JOptionPane.WARNING_MESSAGE - ) + showDialog(title){ dismiss -> + AlertDialog( + onDismissRequest = { }, + icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, + iconContentColor = MaterialTheme.colorScheme.tertiary, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + Button( + onClick = { dismiss() }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.tertiary + ) + ) { + Text("OK") + } + } + ) + } } e?.printStackTrace() } @@ -80,11 +134,28 @@ class Messages { // proper parsing on the command line. Many have \n in them. println("$title: $primary\n$secondary") } else { - EventQueue.invokeLater { - JOptionPane.showMessageDialog( - JFrame(), - Toolkit.formatMessage(primary, secondary), - title, JOptionPane.WARNING_MESSAGE + showDialog(title){ dismiss -> + AlertDialog( + onDismissRequest = { }, + icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, + iconContentColor = MaterialTheme.colorScheme.tertiary, + title = { Text(title) }, + text = { + Column { + Text(primary, fontWeight = FontWeight.Bold) + Text(secondary) + } + }, + confirmButton = { + Button( + onClick = { dismiss() }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.tertiary + ) + ) { + Text("OK") + } + } ) } } @@ -102,10 +173,26 @@ class Messages { if (Base.isCommandLine()) { System.err.println("$title: $message") } else { - JOptionPane.showMessageDialog( - Frame(), message, title, - JOptionPane.ERROR_MESSAGE - ) + showDialog(title){ dismiss -> + AlertDialog( + onDismissRequest = { }, + icon = { Icon(Icons.Default.Error, contentDescription = "Alert!") }, + iconContentColor = MaterialTheme.colorScheme.error, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + Button( + onClick = { dismiss() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + ) { + Text("OK") + } + } + ) + } } e?.printStackTrace() System.exit(1) @@ -139,6 +226,8 @@ class Messages { if (fatal) JOptionPane.ERROR_MESSAGE else JOptionPane.WARNING_MESSAGE ) + + if (fatal) { System.exit(1) } @@ -271,6 +360,56 @@ class Messages { } } +fun showDialog(title: String, content: @Composable (dismiss: () -> Unit) -> Unit) { + ComposeDialog().apply { +// isUndecorated = true +// isTransparent = true + isModal = true + setTitle(title) + size = Dimension(400, 200) + + setContent { + PDETheme { + content(::dispose) + } + } + setLocationRelativeTo(null) + isVisible = true + } +} + +fun main(){ + val types = mapOf( + "message" to { Messages.showMessage("Test Title", "This is a test message.") }, + "warning" to { Messages.showWarning("Test Warning", "This is a test warning.", Exception("dfdsfjk")) }, + "trace" to { Messages.showTrace("Test Trace", "This is a test trace.", Exception("Test Exception"), false) }, + "tiered_warning" to { Messages.showWarningTiered("Test Tiered Warning", "Primary message", "Secondary message", null) }, + "yes_no" to { Messages.showYesNoQuestion(null, "Test Yes/No", "Do you want to continue?", "Choose yes or no.") }, + "custom_question" to { Messages.showCustomQuestion(null, "Test Custom Question", "Choose an option:", "Select one of the options below.", 1, "Option 1", "Option 2", "Option 3") }, + "error" to { Messages.showError("Test Error", "This is a test error.", null) }, + ) + Platform.init() + UIManager.setLookAndFeel(FlatLightLaf()) + application { + val state = rememberWindowState( + size = DpSize(500.dp, 300.dp) + ) + Window(state = state, onCloseRequest = ::exitApplication, title = "Test Messages") { + PDETheme { + Column { + for ((type, action) in types) { + Button(onClick = { action() }, modifier = Modifier.padding(8.dp)) { + Text("Show $type dialog") + } + } + } + } + } + + } + +} + // Helper functions to give the base classes a color fun String.formatClassName() = this .replace("processing.", "") diff --git a/app/src/processing/app/ui/Preferences.kt b/app/src/processing/app/ui/Preferences.kt index 7fd9f56350..12e7c25ce4 100644 --- a/app/src/processing/app/ui/Preferences.kt +++ b/app/src/processing/app/ui/Preferences.kt @@ -101,7 +101,6 @@ class PDEPreferences { style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(top = 42.dp) ) - }, modifier = Modifier .defaultMinSize(minWidth = 200.dp) @@ -289,7 +288,6 @@ private fun PDEPreference.showControl() { show() } } - } /** diff --git a/app/src/processing/app/ui/theme/Colors.kt b/app/src/processing/app/ui/theme/Colors.kt index 61c6d6b55f..af423ba488 100644 --- a/app/src/processing/app/ui/theme/Colors.kt +++ b/app/src/processing/app/ui/theme/Colors.kt @@ -33,54 +33,10 @@ class ProcessingColors{ val foundationDark = Color(0xFF5501a4) val downloadInactive = Color(0xFF8890B3) - val downloadBackgroundActive = Color(0x14508BFF) + val downloadBackgroundActive = Color(0xFF14508B) } } -@Deprecated("Use PDE3LightColor instead") -val PDE2LightColors = Colors( - primary = ProcessingColors.blue, - primaryVariant = ProcessingColors.lightBlue, - onPrimary = ProcessingColors.white, - - secondary = ProcessingColors.deepBlue, - secondaryVariant = ProcessingColors.darkBlue, - onSecondary = ProcessingColors.white, - - background = ProcessingColors.white, - onBackground = ProcessingColors.darkBlue, - - surface = ProcessingColors.lightGray, - onSurface = ProcessingColors.darkerGray, - - error = ProcessingColors.error, - onError = ProcessingColors.white, - - isLight = true, -) - -@Deprecated("Use PDE3DarkColor instead") -val PDE2DarkColors = Colors( - primary = ProcessingColors.deepBlue, - primaryVariant = ProcessingColors.darkBlue, - onPrimary = ProcessingColors.white, - - secondary = ProcessingColors.lightBlue, - secondaryVariant = ProcessingColors.blue, - onSecondary = ProcessingColors.white, - - background = ProcessingColors.veryDarkGray, - onBackground = ProcessingColors.white, - - surface = ProcessingColors.darkerGray, - onSurface = ProcessingColors.lightGray, - - error = ProcessingColors.error, - onError = ProcessingColors.white, - - isLight = false, -) - val PDELightColor = lightColorScheme( primary = ProcessingColors.blue, onPrimary = ProcessingColors.white, diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index 9e41227ed1..554b5f27e7 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -47,6 +47,7 @@ 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 import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.Density @@ -56,6 +57,8 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState +import darkScheme +import lightScheme import processing.app.PreferencesProvider /** @@ -90,12 +93,12 @@ fun PDETheme( PreferencesProvider { LocaleProvider { MaterialTheme( - colorScheme = if(darkTheme) PDEDarkColor else PDELightColor, + colorScheme = if(darkTheme) darkScheme else lightScheme, typography = PDETypography ){ - Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLowest)) { CompositionLocalProvider( - LocalContentColor provides MaterialTheme.colorScheme.onBackground, + LocalContentColor provides MaterialTheme.colorScheme.onSurface, LocalDensity provides Density(1.25f, 1.25f), content = content ) @@ -137,66 +140,79 @@ fun main() { verticalArrangement = Arrangement.spacedBy(16.dp), ) { ComponentPreview("Colors") { + val colors = listOf>( + Triple("Primary", MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary), + Triple("Secondary", MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.onSecondary), + Triple("Tertiary", MaterialTheme.colorScheme.tertiary, MaterialTheme.colorScheme.onTertiary), + Triple("Primary Container", MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.onPrimaryContainer), + Triple("Secondary Container", MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.colorScheme.onSecondaryContainer), + Triple("Tertiary Container", MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.onTertiaryContainer), + Triple("Error Container", MaterialTheme.colorScheme.errorContainer, MaterialTheme.colorScheme.onErrorContainer), + Triple("Background", MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.onBackground), + Triple("Surface", MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.onSurface), + Triple("Surface Variant", MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant), + Triple("Error", MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.onError), + + Triple("Surface Lowest", MaterialTheme.colorScheme.surfaceContainerLowest, MaterialTheme.colorScheme.onSurface), + Triple("Surface Low", MaterialTheme.colorScheme.surfaceContainerLow, MaterialTheme.colorScheme.onSurface), + Triple("Surface", MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.colorScheme.onSurface), + Triple("Surface High", MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface), + Triple("Surface Highest", MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface), + ) Column { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), - onClick = {}) { - Text("Primary", color = MaterialTheme.colorScheme.onPrimary) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), - onClick = {}) { - Text("Secondary", color = MaterialTheme.colorScheme.onSecondary) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), - onClick = {}) { - Text("Tertiary", color = MaterialTheme.colorScheme.onTertiary) +// Button( +// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), +// onClick = {}) { +// Text("Primary", color = MaterialTheme.colorScheme.onPrimary) +// } +// Button( +// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), +// onClick = {}) { +// Text("Secondary", color = MaterialTheme.colorScheme.onSecondary) +// } +// Button( +// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), +// onClick = {}) { +// Text("Tertiary", color = MaterialTheme.colorScheme.onTertiary) +// } + val section = colors.subList(0,3) + for((name, color, onColor) in section){ + Button( + colors = ButtonDefaults.buttonColors(containerColor = color), + onClick = {}) { + Text(name, color = onColor) + } } } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primaryContainer), - onClick = {}) { - Text("Primary Container", color = MaterialTheme.colorScheme.onPrimaryContainer) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), - onClick = {}) { - Text("Secondary Container", color = MaterialTheme.colorScheme.onSecondaryContainer) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), - onClick = {}) { - Text("Tertiary Container", color = MaterialTheme.colorScheme.onTertiaryContainer) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer), - onClick = {}) { - Text("Error Container", color = MaterialTheme.colorScheme.onErrorContainer) + val section = colors.subList(3,7) + for((name, color, onColor) in section){ + Button( + colors = ButtonDefaults.buttonColors(containerColor = color), + onClick = {}) { + Text(name, color = onColor) + } } } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.background), - onClick = {}) { - Text("Background", color = MaterialTheme.colorScheme.onBackground) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surface), - onClick = {}) { - Text("Surface", color = MaterialTheme.colorScheme.onSurface) + val section = colors.subList(7,11) + for((name, color, onColor) in section){ + Button( + colors = ButtonDefaults.buttonColors(containerColor = color), + onClick = {}) { + Text(name, color = onColor) + } } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), - onClick = {}) { - Text("Surface Variant", color = MaterialTheme.colorScheme.onSurfaceVariant) - } - Button( - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - onClick = {}) { - Text("Error", color = MaterialTheme.colorScheme.onError) + } + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + val section = colors.subList(11, 16) + for ((name, color, onColor) in section) { + Button( + colors = ButtonDefaults.buttonColors(containerColor = color), + onClick = {}) { + Text(name, color = onColor) + } } } } @@ -337,10 +353,18 @@ fun main() { } + ComponentPreview("Card") { + Card{ + Text("Hello, Tabs!", modifier = Modifier.padding(20.dp)) + } + } + ComponentPreview("Scrollable View") { } + + ComponentPreview("Tabs") { } diff --git a/app/src/processing/app/ui/theme/m3/Color.kt b/app/src/processing/app/ui/theme/m3/Color.kt new file mode 100644 index 0000000000..b2047ce7e6 --- /dev/null +++ b/app/src/processing/app/ui/theme/m3/Color.kt @@ -0,0 +1,248 @@ +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF525A92) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFF293DAE) +val onPrimaryContainerLight = Color(0xFFABB5FF) +val secondaryLight = Color(0xFF555D7D) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFF8890B3) +val onSecondaryContainerLight = Color(0xFF212946) +val tertiaryLight = Color(0xFF0052CC) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFF0468FF) +val onTertiaryContainerLight = Color(0xFFFBF9FF) +val errorLight = Color(0xFFBB0026) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFE41D37) +val onErrorContainerLight = Color(0xFFFFFBFF) +val backgroundLight = Color(0xFFFBF8FF) +val onBackgroundLight = Color(0xFF1A1B22) +val surfaceLight = Color(0xFFFDF8F8) +val onSurfaceLight = Color(0xFF1C1B1C) +val surfaceVariantLight = Color(0xFFE4E1E8) +val onSurfaceVariantLight = Color(0xFF47464B) +val outlineLight = Color(0xFF77767C) +val outlineVariantLight = Color(0xFFC8C5CB) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF313030) +val inverseOnSurfaceLight = Color(0xFFF4F0EF) +val inversePrimaryLight = Color(0xFFBBC3FF) +val surfaceDimLight = Color(0xFFDDD9D9) +val surfaceBrightLight = Color(0xFFFDF8F8) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF7F3F2) +val surfaceContainerLight = Color(0xFFF1EDED) +val surfaceContainerHighLight = Color(0xFFEBE7E7) +val surfaceContainerHighestLight = Color(0xFFE5E2E1) + +val primaryLightMediumContrast = Color(0xFF525A92) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF293DAE) +val onPrimaryContainerLightMediumContrast = Color(0xFFE3E4FF) +val secondaryLightMediumContrast = Color(0xFF2D3553) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF646C8D) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF003080) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF0062F3) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF730013) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFD91030) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFFBF8FF) +val onBackgroundLightMediumContrast = Color(0xFF1A1B22) +val surfaceLightMediumContrast = Color(0xFFFDF8F8) +val onSurfaceLightMediumContrast = Color(0xFF111111) +val surfaceVariantLightMediumContrast = Color(0xFFE4E1E8) +val onSurfaceVariantLightMediumContrast = Color(0xFF36363B) +val outlineLightMediumContrast = Color(0xFF525257) +val outlineVariantLightMediumContrast = Color(0xFF6D6C72) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF313030) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF4F0EF) +val inversePrimaryLightMediumContrast = Color(0xFFBBC3FF) +val surfaceDimLightMediumContrast = Color(0xFFC9C6C5) +val surfaceBrightLightMediumContrast = Color(0xFFFDF8F8) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF7F3F2) +val surfaceContainerLightMediumContrast = Color(0xFFEBE7E7) +val surfaceContainerHighLightMediumContrast = Color(0xFFE0DCDC) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD4D1D0) + +val primaryLightHighContrast = Color(0xFF525A92) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF283CAD) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF222B48) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF404867) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF00276B) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF0042A8) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF60000E) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF97001C) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFFBF8FF) +val onBackgroundLightHighContrast = Color(0xFF1A1B22) +val surfaceLightHighContrast = Color(0xFFFDF8F8) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE4E1E8) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF2C2C30) +val outlineVariantLightHighContrast = Color(0xFF49494E) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF313030) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFBBC3FF) +val surfaceDimLightHighContrast = Color(0xFFBBB8B8) +val surfaceBrightLightHighContrast = Color(0xFFFDF8F8) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF4F0EF) +val surfaceContainerLightHighContrast = Color(0xFFE5E2E1) +val surfaceContainerHighLightHighContrast = Color(0xFFD7D3D3) +val surfaceContainerHighestLightHighContrast = Color(0xFFC9C6C5) + +val primaryDark = Color(0xFFBBC3FF) +val onPrimaryDark = Color(0xFF001D93) +val primaryContainerDark = Color(0xFF293DAE) +val onPrimaryContainerDark = Color(0xFFABB5FF) +val secondaryDark = Color(0xFFBDC5EA) +val onSecondaryDark = Color(0xFF272F4D) +val secondaryContainerDark = Color(0xFF8890B3) +val onSecondaryContainerDark = Color(0xFF212946) +val tertiaryDark = Color(0xFFB2C5FF) +val onTertiaryDark = Color(0xFF002B74) +val tertiaryContainerDark = Color(0xFF0468FF) +val onTertiaryContainerDark = Color(0xFFFBF9FF) +val errorDark = Color(0xFFFFB3B0) +val onErrorDark = Color(0xFF680010) +val errorContainerDark = Color(0xFFFF5359) +val onErrorContainerDark = Color(0xFF220002) +val backgroundDark = Color(0xFF12131A) +val onBackgroundDark = Color(0xFFE3E1EB) +val surfaceDark = Color(0xFF141313) +val onSurfaceDark = Color(0xFFE5E2E1) +val surfaceVariantDark = Color(0xFF47464B) +val onSurfaceVariantDark = Color(0xFFC8C5CB) +val outlineDark = Color(0xFF919096) +val outlineVariantDark = Color(0xFF47464B) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE5E2E1) +val inverseOnSurfaceDark = Color(0xFF313030) +val inversePrimaryDark = Color(0xFF4053C3) +val surfaceDimDark = Color(0xFF141313) +val surfaceBrightDark = Color(0xFF3A3939) +val surfaceContainerLowestDark = Color(0xFF0E0E0E) +val surfaceContainerLowDark = Color(0xFF1C1B1C) +val surfaceContainerDark = Color(0xFF201F20) +val surfaceContainerHighDark = Color(0xFF2B2A2A) +val surfaceContainerHighestDark = Color(0xFF353435) + +val primaryDarkMediumContrast = Color(0xFFBBC3FF) +val onPrimaryDarkMediumContrast = Color(0xFF001677) +val primaryContainerDarkMediumContrast = Color(0xFF7587FA) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFD4DBFF) +val onSecondaryDarkMediumContrast = Color(0xFF1C2441) +val secondaryContainerDarkMediumContrast = Color(0xFF8890B3) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFD2DBFF) +val onTertiaryDarkMediumContrast = Color(0xFF00215E) +val tertiaryContainerDarkMediumContrast = Color(0xFF5D8BFF) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CF) +val onErrorDarkMediumContrast = Color(0xFF54000B) +val errorContainerDarkMediumContrast = Color(0xFFFF5359) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF12131A) +val onBackgroundDarkMediumContrast = Color(0xFFE3E1EB) +val surfaceDarkMediumContrast = Color(0xFF141313) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF47464B) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDEDBE1) +val outlineDarkMediumContrast = Color(0xFFB3B1B7) +val outlineVariantDarkMediumContrast = Color(0xFF918F95) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE5E2E1) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF2B2A2A) +val inversePrimaryDarkMediumContrast = Color(0xFF263AAC) +val surfaceDimDarkMediumContrast = Color(0xFF141313) +val surfaceBrightDarkMediumContrast = Color(0xFF454444) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF080707) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1E1D1E) +val surfaceContainerDarkMediumContrast = Color(0xFF282828) +val surfaceContainerHighDarkMediumContrast = Color(0xFF333232) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3E3D3D) + +val primaryDarkHighContrast = Color(0xFFBBC3FF) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFB6BFFF) +val onPrimaryContainerDarkHighContrast = Color(0xFF000533) +val secondaryDarkHighContrast = Color(0xFFEEEFFF) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFB9C1E6) +val onSecondaryContainerDarkHighContrast = Color(0xFF020926) +val tertiaryDarkHighContrast = Color(0xFFEDEFFF) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFADC1FF) +val onTertiaryContainerDarkHighContrast = Color(0xFF000926) +val errorDarkHighContrast = Color(0xFFFFECEA) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFADAB) +val onErrorContainerDarkHighContrast = Color(0xFF220002) +val backgroundDarkHighContrast = Color(0xFF12131A) +val onBackgroundDarkHighContrast = Color(0xFFE3E1EB) +val surfaceDarkHighContrast = Color(0xFF141313) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF47464B) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFF2EFF5) +val outlineVariantDarkHighContrast = Color(0xFFC4C2C8) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE5E2E1) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF263AAC) +val surfaceDimDarkHighContrast = Color(0xFF141313) +val surfaceBrightDarkHighContrast = Color(0xFF515050) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF201F20) +val surfaceContainerDarkHighContrast = Color(0xFF313030) +val surfaceContainerHighDarkHighContrast = Color(0xFF3C3B3B) +val surfaceContainerHighestDarkHighContrast = Color(0xFF484646) + +val warningLight = Color(0xFF765B0B) +val onWarningLight = Color(0xFFFFFFFF) +val warningContainerLight = Color(0xFFFFDF97) +val onWarningContainerLight = Color(0xFF5A4300) + +val warningLightMediumContrast = Color(0xFF453400) +val onWarningLightMediumContrast = Color(0xFFFFFFFF) +val warningContainerLightMediumContrast = Color(0xFF86691C) +val onWarningContainerLightMediumContrast = Color(0xFFFFFFFF) + +val warningLightHighContrast = Color(0xFF392A00) +val onWarningLightHighContrast = Color(0xFFFFFFFF) +val warningContainerLightHighContrast = Color(0xFF5D4600) +val onWarningContainerLightHighContrast = Color(0xFFFFFFFF) + +val warningDark = Color(0xFFE6C26C) +val onWarningDark = Color(0xFF3E2E00) +val warningContainerDark = Color(0xFF5A4300) +val onWarningContainerDark = Color(0xFFFFDF97) + +val warningDarkMediumContrast = Color(0xFFFED87F) +val onWarningDarkMediumContrast = Color(0xFF312400) +val warningContainerDarkMediumContrast = Color(0xFFAD8D3D) +val onWarningContainerDarkMediumContrast = Color(0xFF000000) + +val warningDarkHighContrast = Color(0xFFFFEECF) +val onWarningDarkHighContrast = Color(0xFF000000) +val warningContainerDarkHighContrast = Color(0xFFE2BE69) +val onWarningContainerDarkHighContrast = Color(0xFF110A00) + diff --git a/app/src/processing/app/ui/theme/m3/Theme.kt b/app/src/processing/app/ui/theme/m3/Theme.kt new file mode 100644 index 0000000000..d1b2f403a8 --- /dev/null +++ b/app/src/processing/app/ui/theme/m3/Theme.kt @@ -0,0 +1,301 @@ + + +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class ExtendedColorScheme( + val warning: ColorFamily, +) + +val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) + +private val mediumContrastLightColorScheme = lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, +) + +private val highContrastLightColorScheme = lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, +) + +private val mediumContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, +) + +private val highContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, +) + +val extendedLight = ExtendedColorScheme( + warning = ColorFamily( + warningLight, + onWarningLight, + warningContainerLight, + onWarningContainerLight, + ), +) + +val extendedDark = ExtendedColorScheme( + warning = ColorFamily( + warningDark, + onWarningDark, + warningContainerDark, + onWarningContainerDark, + ), +) + +val extendedLightMediumContrast = ExtendedColorScheme( + warning = ColorFamily( + warningLightMediumContrast, + onWarningLightMediumContrast, + warningContainerLightMediumContrast, + onWarningContainerLightMediumContrast, + ), +) + +val extendedLightHighContrast = ExtendedColorScheme( + warning = ColorFamily( + warningLightHighContrast, + onWarningLightHighContrast, + warningContainerLightHighContrast, + onWarningContainerLightHighContrast, + ), +) + +val extendedDarkMediumContrast = ExtendedColorScheme( + warning = ColorFamily( + warningDarkMediumContrast, + onWarningDarkMediumContrast, + warningContainerDarkMediumContrast, + onWarningContainerDarkMediumContrast, + ), +) + +val extendedDarkHighContrast = ExtendedColorScheme( + warning = ColorFamily( + warningDarkHighContrast, + onWarningDarkHighContrast, + warningContainerDarkHighContrast, + onWarningContainerDarkHighContrast, + ), +) + +@Immutable +data class ColorFamily( + val color: Color, + val onColor: Color, + val colorContainer: Color, + val onColorContainer: Color +) \ No newline at end of file From ca298009e146f2b2d24cc4ce423d8fb9ecde8197 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 23 Oct 2025 21:09:54 +0200 Subject: [PATCH 06/23] Add PDEWelcome Composable UI screen Introduces a new PDEWelcome.kt file with a Composable UI for the Processing welcome screen. Includes layout with buttons for language selection, new sketch, examples, and sketchbook, as well as a placeholder for right-side content and a main entry point for launching the window. --- app/src/processing/app/ui/PDEWelcome.kt | 112 +++++++++++++++++++++++ app/src/processing/app/ui/theme/Theme.kt | 2 +- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 app/src/processing/app/ui/PDEWelcome.kt diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt new file mode 100644 index 0000000000..d061107ec4 --- /dev/null +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -0,0 +1,112 @@ +package processing.app.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Drafts +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Language +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MultiChoiceSegmentedButtonRow +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.application +import processing.app.ui.theme.LocalLocale +import processing.app.ui.theme.PDEComposeWindow +import processing.app.ui.theme.PDETheme + +@Composable +fun PDEWelcome() { + Row (modifier = Modifier.fillMaxSize()){ + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .weight(0.8f) + .padding(32.dp) + ) { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + ){ + OutlinedButton( + onClick = {}, + modifier = Modifier.height(30.dp) + ) { + Icon(Icons.Default.Language, contentDescription = "") +// Text(LocalLocale.current.locale.displayName, style = MaterialTheme.typography.labelSmall) + Icon(Icons.Default.ArrowDropDown, contentDescription = "") + } + } + Row (horizontalArrangement = Arrangement.spacedBy(16.dp)){ + Box( + modifier = Modifier + .size(50.dp) + .background(MaterialTheme.colorScheme.primary) + ) + Text( + text = "Welcome to Processing!", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier + .align(Alignment.Bottom) + ) + } + Column { + TextButton(onClick = {}) { + Icon(Icons.Default.Drafts, contentDescription = "") + Text("New Empty Sketch") + } + TextButton(onClick = {}) { + Icon(Icons.Default.Image, contentDescription = "") + Text("Open Examples") + } + TextButton(onClick = {}) { + Icon(Icons.Default.Folder, contentDescription = "") + Text("Sketchbook") + } + } + } + VerticalDivider() + Column(modifier = Modifier + .sizeIn(minWidth = 250.dp) + ) { + Text("Right Side Content", style = MaterialTheme.typography.bodyLarge) + } + } +} + + + +fun main(){ + application { + PDEComposeWindow(titleKey = "welcome.title", size = DpSize(800.dp, 600.dp), fullWindowContent = true) { + PDETheme(darkTheme = false) { + PDEWelcome() + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index 554b5f27e7..7790aba776 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -99,7 +99,7 @@ fun PDETheme( Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLowest)) { CompositionLocalProvider( LocalContentColor provides MaterialTheme.colorScheme.onSurface, - LocalDensity provides Density(1.25f, 1.25f), +// LocalDensity provides Density(1.25f, 1.25f), content = content ) } From 2fc56820fa8606f3de7ab621900e186ce188906e Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 27 Oct 2025 08:39:10 +0100 Subject: [PATCH 07/23] Initial layout --- .../processing/app/ui/WebFrame.java | 0 .../processing/app/ui/Welcome.java | 0 app/src/processing/app/Base.java | 8 +- app/src/processing/app/ui/PDEWelcome.kt | 290 +++++++++++++++--- java/src/processing/mode/java/JavaEditor.java | 8 +- 5 files changed, 250 insertions(+), 56 deletions(-) rename app/{src => ant}/processing/app/ui/WebFrame.java (100%) rename app/{src => ant}/processing/app/ui/Welcome.java (100%) diff --git a/app/src/processing/app/ui/WebFrame.java b/app/ant/processing/app/ui/WebFrame.java similarity index 100% rename from app/src/processing/app/ui/WebFrame.java rename to app/ant/processing/app/ui/WebFrame.java diff --git a/app/src/processing/app/ui/Welcome.java b/app/ant/processing/app/ui/Welcome.java similarity index 100% rename from app/src/processing/app/ui/Welcome.java rename to app/ant/processing/app/ui/Welcome.java diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index e3eae12fb8..43f089b288 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -375,13 +375,7 @@ static private void handleWelcomeScreen(Base base) { // Needs to be shown after the first editor window opens, so that it // shows up on top, and doesn't prevent an editor window from opening. if (Preferences.getBoolean("welcome.four.show")) { - try { - new Welcome(base); - } catch (IOException e) { - Messages.showTrace("Unwelcoming", - "Please report this error to\n" + - "https://github.com/processing/processing4/issues", e, false); - } + PDEWelcomeKt.showWelcomeScreen(); } } diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index d061107ec4..35bd64b778 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -1,10 +1,16 @@ package processing.app.ui -import androidx.compose.foundation.background +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -13,86 +19,280 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Drafts import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Language +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MultiChoiceSegmentedButtonRow -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application -import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.PDEComposeWindow +import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme +import java.awt.Dimension @Composable fun PDEWelcome() { - Row (modifier = Modifier.fillMaxSize()){ + Row( + modifier = Modifier.fillMaxSize(), + ){ + val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) + val xsModifier = Modifier + .defaultMinSize(minHeight = 1.dp) + .height(32.dp) + Column( - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier + .fillMaxHeight() .weight(0.8f) - .padding(32.dp) + .padding( + top = 48.dp, + start = 56.dp, + end = 64.dp, + bottom = 56.dp + ) ) { - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() + Row ( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() ){ - OutlinedButton( - onClick = {}, - modifier = Modifier.height(30.dp) - ) { - Icon(Icons.Default.Language, contentDescription = "") -// Text(LocalLocale.current.locale.displayName, style = MaterialTheme.typography.labelSmall) - Icon(Icons.Default.ArrowDropDown, contentDescription = "") - } - } - Row (horizontalArrangement = Arrangement.spacedBy(16.dp)){ - Box( + Image( + painter = painterResource("logo.svg"), modifier = Modifier - .size(50.dp) - .background(MaterialTheme.colorScheme.primary) + .size(75.dp), + contentDescription = "Processing Logo" ) Text( text = "Welcome to Processing!", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier - .align(Alignment.Bottom) + .align(Alignment.CenterVertically) ) } - Column { - TextButton(onClick = {}) { - Icon(Icons.Default.Drafts, contentDescription = "") - Text("New Empty Sketch") + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier +// .background(Color.Blue) + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(vertical = 12.dp) + ) { + val colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxHeight() + ) { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + TextButton( + onClick = {}, + colors = colors, + modifier = Modifier + .sizeIn(minHeight = 56.dp) + ) { + Icon(Icons.Default.Drafts, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text("New Empty Sketch") + } + TextButton( + onClick = {}, + colors = colors, + modifier = Modifier + .sizeIn(minHeight = 56.dp) + ) { + Icon(Icons.Default.Image, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text("Open Examples") + } + } } - TextButton(onClick = {}) { - Icon(Icons.Default.Image, contentDescription = "") - Text("Open Examples") + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxHeight() + ) { + ProvideTextStyle(MaterialTheme.typography.labelLarge) { + + TextButton( + onClick = {}, + contentPadding = xsPadding, + colors = colors, + modifier = xsModifier + ) { + Icon(Icons.Default.Folder, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text("Sketchbook", modifier = Modifier.align(Alignment.CenterVertically)) + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + colors = colors, + modifier = xsModifier + ) { + Icon(Icons.Default.Folder, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text("Settings", modifier = Modifier.align(Alignment.CenterVertically)) + } + Button( + onClick = {}, + contentPadding = xsPadding, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary + ), + modifier = xsModifier + ) { + Icon(Icons.Default.Folder, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text("Show this window on startup", modifier = Modifier.align(Alignment.CenterVertically)) + } + } } - TextButton(onClick = {}) { - Icon(Icons.Default.Folder, contentDescription = "") - Text("Sketchbook") + } + Card( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ){ + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .padding( + top = 18.dp, + end = 24.dp, + bottom = 24.dp, + start = 24.dp + ) + ) { + val colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ProvideTextStyle(MaterialTheme .typography.labelLarge) { + Column { + Text( + "Resources", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + modifier = Modifier.padding(start = 8.dp) + ) + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Video Course") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Get Started") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Tutorials") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Documentation") + } + } + Column { + Text( + "Join our community", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + modifier = Modifier.padding(start = 8.dp) + ) + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Video Course") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Get Started") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Tutorials") + } + TextButton( + onClick = {}, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text("Documentation") + } + } + } } } } VerticalDivider() Column(modifier = Modifier - .sizeIn(minWidth = 250.dp) + .sizeIn(minWidth = 350.dp) ) { Text("Right Side Content", style = MaterialTheme.typography.bodyLarge) } @@ -100,10 +300,16 @@ fun PDEWelcome() { } +fun showWelcomeScreen(){ + PDESwingWindow(titleKey = "welcome.title", size = Dimension(970, 570), fullWindowContent = true) { + PDEWelcome() + } +} + fun main(){ application { - PDEComposeWindow(titleKey = "welcome.title", size = DpSize(800.dp, 600.dp), fullWindowContent = true) { + PDEComposeWindow(titleKey = "welcome.title", size = DpSize(970.dp, 570.dp), fullWindowContent = true) { PDETheme(darkTheme = false) { PDEWelcome() } diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 3fab2c8b17..6998516b32 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -288,13 +288,7 @@ public JMenu buildHelpMenu() { item = new JMenuItem(Language.text("menu.help.welcome")); item.addActionListener(e -> { - try { - new Welcome(base); - } catch (IOException ioe) { - Messages.showWarning("Unwelcome Error", - "Please report this error to\n" + - "https://github.com/processing/processing4/issues", ioe); - } + PDEWelcomeKt.showWelcomeScreen(); }); menu.add(item); From 60372758e9c5339dfeef0e65979c335677378384 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 27 Oct 2025 09:58:35 +0100 Subject: [PATCH 08/23] Revamp welcome screen UI and add social icons Refactors the PDEWelcome screen to improve layout, update button icons, and add support for Discord, GitHub, and Instagram SVG icons. The welcome screen now receives a Base instance for proper action handling, and new methods replace deprecated ones in Base.java. Updates related menu actions to pass the Base instance as needed. --- app/src/main/resources/icons/Discord.svg | 3 + app/src/main/resources/icons/GitHub.svg | 10 + app/src/main/resources/icons/Instagram.svg | 3 + app/src/processing/app/Base.java | 6 +- app/src/processing/app/ui/PDEWelcome.kt | 233 ++++++++++++------ java/src/processing/mode/java/JavaEditor.java | 2 +- 6 files changed, 177 insertions(+), 80 deletions(-) create mode 100644 app/src/main/resources/icons/Discord.svg create mode 100644 app/src/main/resources/icons/GitHub.svg create mode 100644 app/src/main/resources/icons/Instagram.svg diff --git a/app/src/main/resources/icons/Discord.svg b/app/src/main/resources/icons/Discord.svg new file mode 100644 index 0000000000..54f918b869 --- /dev/null +++ b/app/src/main/resources/icons/Discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/resources/icons/GitHub.svg b/app/src/main/resources/icons/GitHub.svg new file mode 100644 index 0000000000..39b263b230 --- /dev/null +++ b/app/src/main/resources/icons/GitHub.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/resources/icons/Instagram.svg b/app/src/main/resources/icons/Instagram.svg new file mode 100644 index 0000000000..abb51a22e5 --- /dev/null +++ b/app/src/main/resources/icons/Instagram.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 43f089b288..fe20b82da1 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -375,7 +375,7 @@ static private void handleWelcomeScreen(Base base) { // Needs to be shown after the first editor window opens, so that it // shows up on top, and doesn't prevent an editor window from opening. if (Preferences.getBoolean("welcome.four.show")) { - PDEWelcomeKt.showWelcomeScreen(); + PDEWelcomeKt.showWelcomeScreen(base); } } @@ -603,7 +603,7 @@ public JMenu initDefaultFileMenu() { defaultFileMenu.add(item); item = Toolkit.newJMenuItemShift(Language.text("menu.file.examples"), 'O'); - item.addActionListener(e -> thinkDifferentExamples()); + item.addActionListener(e -> showExamplesFrame()); defaultFileMenu.add(item); return defaultFileMenu; @@ -1879,7 +1879,7 @@ public void handleRestart() { // } - public void thinkDifferentExamples() { + public void showExamplesFrame() { nextMode.showExamplesFrame(); } diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 35bd64b778..60a552a353 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -1,14 +1,13 @@ package processing.app.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -19,10 +18,17 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Drafts -import androidx.compose.material.icons.filled.Folder -import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Book +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material.icons.outlined.FolderOpen +import androidx.compose.material.icons.outlined.FolderSpecial +import androidx.compose.material.icons.outlined.NoteAdd +import androidx.compose.material.icons.outlined.PinDrop +import androidx.compose.material.icons.outlined.School +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.SmartDisplay import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -39,16 +45,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application +import processing.app.Base +import processing.app.LocalPreferences +import processing.app.Messages +import processing.app.Platform import processing.app.ui.theme.PDEComposeWindow import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme -import java.awt.Dimension +import processing.app.ui.theme.toDimension @Composable -fun PDEWelcome() { +fun PDEWelcome(base: Base? = null) { Row( modifier = Modifier.fillMaxSize(), ){ @@ -56,6 +65,7 @@ fun PDEWelcome() { val xsModifier = Modifier .defaultMinSize(minHeight = 1.dp) .height(32.dp) + val textColor = if(isSystemInDarkTheme()) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSecondaryContainer Column( verticalArrangement = Arrangement.SpaceBetween, @@ -82,7 +92,7 @@ fun PDEWelcome() { Text( text = "Welcome to Processing!", style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSecondaryContainer, + color = textColor, modifier = Modifier .align(Alignment.CenterVertically) ) @@ -90,13 +100,12 @@ fun PDEWelcome() { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier -// .background(Color.Blue) .fillMaxWidth() .height(IntrinsicSize.Min) .padding(vertical = 12.dp) ) { val colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.onSecondaryContainer + contentColor = textColor ) Column( verticalArrangement = Arrangement.SpaceBetween, @@ -105,22 +114,28 @@ fun PDEWelcome() { ) { ProvideTextStyle(MaterialTheme.typography.titleMedium) { TextButton( - onClick = {}, + onClick = { + base?.handleNew() ?: noBaseWarning() + }, colors = colors, modifier = Modifier .sizeIn(minHeight = 56.dp) ) { - Icon(Icons.Default.Drafts, contentDescription = "") + Icon(Icons.Outlined.NoteAdd, contentDescription = "") Spacer(Modifier.width(12.dp)) Text("New Empty Sketch") } TextButton( - onClick = {}, + onClick = { + base?.let{ + base.showExamplesFrame() + } ?: noBaseWarning() + }, colors = colors, modifier = Modifier .sizeIn(minHeight = 56.dp) ) { - Icon(Icons.Default.Image, contentDescription = "") + Icon(Icons.Outlined.FolderSpecial, contentDescription = "") Spacer(Modifier.width(12.dp)) Text("Open Examples") } @@ -132,37 +147,48 @@ fun PDEWelcome() { .fillMaxHeight() ) { ProvideTextStyle(MaterialTheme.typography.labelLarge) { - TextButton( - onClick = {}, + onClick = { + base?.let{ + base.showSketchbookFrame() + } ?: noBaseWarning() + }, contentPadding = xsPadding, colors = colors, modifier = xsModifier ) { - Icon(Icons.Default.Folder, contentDescription = "") + Icon(Icons.Outlined.FolderOpen, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) Text("Sketchbook", modifier = Modifier.align(Alignment.CenterVertically)) } TextButton( - onClick = {}, + onClick = { + base?.let{ + base.handlePrefs() + } ?: noBaseWarning() + }, contentPadding = xsPadding, colors = colors, modifier = xsModifier ) { - Icon(Icons.Default.Folder, contentDescription = "") + Icon(Icons.Outlined.Settings, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) Text("Settings", modifier = Modifier.align(Alignment.CenterVertically)) } + val preferences = LocalPreferences.current + val showOnStartup = preferences["welcome.four.show"].toBoolean() Button( - onClick = {}, + onClick = { + preferences["welcome.four.show"] = (!showOnStartup).toString() + }, contentPadding = xsPadding, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary + containerColor = if(showOnStartup) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceContainer, + contentColor = if (showOnStartup) MaterialTheme.colorScheme.onTertiary else MaterialTheme.colorScheme.onSurface ), modifier = xsModifier ) { - Icon(Icons.Default.Folder, contentDescription = "") + Icon(if(showOnStartup) Icons.Default.Check else Icons.Default.Close, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) Text("Show this window on startup", modifier = Modifier.align(Alignment.CenterVertically)) } @@ -191,7 +217,7 @@ fun PDEWelcome() { val colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) - ProvideTextStyle(MaterialTheme .typography.labelLarge) { + ProvideTextStyle(MaterialTheme.typography.labelLarge) { Column { Text( "Resources", @@ -199,42 +225,50 @@ fun PDEWelcome() { modifier = Modifier.padding(start = 8.dp) ) TextButton( - onClick = {}, + onClick = { + Platform.openURL("https://hello.processing.org") + }, contentPadding = xsPadding, modifier = xsModifier, colors = colors ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Icon(Icons.Outlined.SmartDisplay, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) Text("Video Course") } TextButton( - onClick = {}, + onClick = { + Platform.openURL("https://processing.org/tutorials/gettingstarted") + }, contentPadding = xsPadding, modifier = xsModifier, colors = colors ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Icon(Icons.Outlined.PinDrop, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) Text("Get Started") } TextButton( - onClick = {}, + onClick = { + Platform.openURL("https://processing.org/tutorials") + }, contentPadding = xsPadding, modifier = xsModifier, colors = colors ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Icon(Icons.Outlined.School, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) Text("Tutorials") } TextButton( - onClick = {}, + onClick = { + Platform.openURL("https://processing.org/reference") + }, contentPadding = xsPadding, modifier = xsModifier, colors = colors ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Icon(Icons.Outlined.Book, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) Text("Documentation") } @@ -245,45 +279,79 @@ fun PDEWelcome() { style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), modifier = Modifier.padding(start = 8.dp) ) - TextButton( - onClick = {}, - contentPadding = xsPadding, - modifier = xsModifier, - colors = colors + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .fillMaxWidth() ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(4.dp)) - Text("Video Course") - } - TextButton( - onClick = {}, - contentPadding = xsPadding, - modifier = xsModifier, - colors = colors - ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(4.dp)) - Text("Get Started") - } - TextButton( - onClick = {}, - contentPadding = xsPadding, - modifier = xsModifier, - colors = colors - ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(4.dp)) - Text("Tutorials") - } - TextButton( - onClick = {}, - contentPadding = xsPadding, - modifier = xsModifier, - colors = colors - ) { - Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(4.dp)) - Text("Documentation") + Column { + TextButton( + onClick = { + Platform.openURL("https://discourse.processing.org") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon( + Icons.Outlined.ChatBubbleOutline, + contentDescription = "", + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.width(4.dp)) + Text("Forum") + } + TextButton( + onClick = { + Platform.openURL("https://discord.processing.org") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon( + painterResource("icons/Discord.svg"), + contentDescription = "", + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.width(4.dp)) + Text("Discord") + } + } + Column { + TextButton( + onClick = { + Platform.openURL("https://www.instagram.com/processing_core/") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon( + painterResource("icons/GitHub.svg"), + contentDescription = "", + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.width(4.dp)) + Text("GitHub") + } + TextButton( + onClick = { + Platform.openURL("https://github.com/processing/processing4") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon( + painterResource("icons/Instagram.svg"), + contentDescription = "", + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.width(4.dp)) + Text("Instagram") + } + } } } } @@ -299,17 +367,30 @@ fun PDEWelcome() { } } +fun noBaseWarning() { + Messages.showWarning( + "No Base", + "No Base instance provided, this ui is likely being previewed." + ) +} -fun showWelcomeScreen(){ - PDESwingWindow(titleKey = "welcome.title", size = Dimension(970, 570), fullWindowContent = true) { - PDEWelcome() +val size = DpSize(970.dp, 550.dp) + +fun showWelcomeScreen(base: Base? = null) { + PDESwingWindow(titleKey = "welcome.title", size = size.toDimension(), fullWindowContent = true) { + PDEWelcome(base) } } fun main(){ application { - PDEComposeWindow(titleKey = "welcome.title", size = DpSize(970.dp, 570.dp), fullWindowContent = true) { + PDEComposeWindow(titleKey = "welcome.title", size = size, fullWindowContent = true) { + PDETheme(darkTheme = true) { + PDEWelcome() + } + } + PDEComposeWindow(titleKey = "welcome.title", size = size, fullWindowContent = true) { PDETheme(darkTheme = false) { PDEWelcome() } diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 6998516b32..7ce9e45be7 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -288,7 +288,7 @@ public JMenu buildHelpMenu() { item = new JMenuItem(Language.text("menu.help.welcome")); item.addActionListener(e -> { - PDEWelcomeKt.showWelcomeScreen(); + PDEWelcomeKt.showWelcomeScreen(base); }); menu.add(item); From 33ad602ebea7db3b7c92e1e5b72798a154521c4c Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 27 Oct 2025 11:58:16 +0100 Subject: [PATCH 09/23] Add example previews to welcome screen Replaces placeholder text on the right side of the PDEWelcome screen with a LazyColumn displaying example sketches. Each example attempts to show a preview image if available, or a placeholder icon otherwise. Introduces an Example data class and related image loading logic. --- app/src/processing/app/ui/PDEWelcome.kt | 68 ++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 60a552a353..0f5ac0474c 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -1,13 +1,16 @@ package processing.app.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -17,6 +20,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close @@ -33,6 +39,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle @@ -40,8 +47,13 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +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.graphics.ImageBitmap +import androidx.compose.ui.graphics.decodeToImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize @@ -55,6 +67,8 @@ import processing.app.ui.theme.PDEComposeWindow import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme import processing.app.ui.theme.toDimension +import java.io.File +import java.nio.file.Path @Composable fun PDEWelcome(base: Base? = null) { @@ -362,11 +376,63 @@ fun PDEWelcome(base: Base? = null) { Column(modifier = Modifier .sizeIn(minWidth = 350.dp) ) { - Text("Right Side Content", style = MaterialTheme.typography.bodyLarge) + val examples = listOf( + Example(Platform.getContentFile("modes/java/examples/Basics/Arrays/Array")), + Example(Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective")), + Example(Platform.getContentFile("modes/java/examples/Basics/Color/Brightness")), + Example(Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")), + ) + LazyColumn( + state = rememberLazyListState( + initialFirstVisibleItemScrollOffset = 150 + ), + modifier = Modifier.width(350.dp) + ) { + items(examples) { example -> + Box(Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxSize() + .aspectRatio(16 / 9f) + ){ + val image = remember { + val name = example.path.name + File(example.path,"$name.png").takeIf { it.exists() } + } + if(image == null){ + Icon( + painter = painterResource("logo.svg"), + modifier = Modifier + .size(75.dp) + .align(Alignment.Center) + , + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + contentDescription = "Processing Logo" + ) + HorizontalDivider() + }else { + val imageBitmap: ImageBitmap = remember(image) { + image.inputStream().readAllBytes().decodeToImageBitmap() + } + Image( + painter = BitmapPainter(imageBitmap), + modifier = Modifier +// .fillMaxSize() + , + contentDescription = example.path.name + ) + } + + } + } + } } } } +data class Example( + val path: File, +) + fun noBaseWarning() { Messages.showWarning( "No Base", From 20aac8164f366a25c3918f5c86d48daa9d6e75c3 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 27 Oct 2025 12:26:09 +0100 Subject: [PATCH 10/23] Add hover-activated play button to example previews Introduced a hover effect on example preview images in the welcome screen, displaying a play button that opens the example when clicked. Refactored title key usage for consistency. --- app/src/processing/app/ui/PDEWelcome.kt | 50 +++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 0f5ac0474c..7a414a3142 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -26,12 +26,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material.icons.outlined.FolderOpen import androidx.compose.material.icons.outlined.FolderSpecial import androidx.compose.material.icons.outlined.NoteAdd import androidx.compose.material.icons.outlined.PinDrop +import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.School import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.SmartDisplay @@ -39,21 +41,33 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider 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.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.decodeToImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize @@ -70,6 +84,7 @@ import processing.app.ui.theme.toDimension import java.io.File import java.nio.file.Path +@OptIn(ExperimentalComposeUiApi::class) @Composable fun PDEWelcome(base: Base? = null) { Row( @@ -389,10 +404,17 @@ fun PDEWelcome(base: Base? = null) { modifier = Modifier.width(350.dp) ) { items(examples) { example -> + var hovered by remember { mutableStateOf(false) } Box(Modifier .background(MaterialTheme.colorScheme.surfaceVariant) .fillMaxSize() .aspectRatio(16 / 9f) + .onPointerEvent(PointerEventType.Enter){ + hovered = true + } + .onPointerEvent(PointerEventType.Exit){ + hovered = false + } ){ val image = remember { val name = example.path.name @@ -421,7 +443,26 @@ fun PDEWelcome(base: Base? = null) { contentDescription = example.path.name ) } - + if(hovered) { + FilledTonalIconButton( + onClick = { + base?.let { + base.handleOpen(example.path.resolve("${example.path.name}.pde").absolutePath) + } ?: noBaseWarning() + }, modifier = Modifier + .align(Alignment.Center), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary, + ) + ) { + Icon( + Icons.Filled.PlayArrow, + contentDescription = "Open Example", + tint = Color.White + ) + } + } } } } @@ -441,9 +482,10 @@ fun noBaseWarning() { } val size = DpSize(970.dp, 550.dp) +val titleKey = "menu.help.welcome" fun showWelcomeScreen(base: Base? = null) { - PDESwingWindow(titleKey = "welcome.title", size = size.toDimension(), fullWindowContent = true) { + PDESwingWindow(titleKey = titleKey, size = size.toDimension(), fullWindowContent = true) { PDEWelcome(base) } } @@ -451,12 +493,12 @@ fun showWelcomeScreen(base: Base? = null) { fun main(){ application { - PDEComposeWindow(titleKey = "welcome.title", size = size, fullWindowContent = true) { + PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { PDETheme(darkTheme = true) { PDEWelcome() } } - PDEComposeWindow(titleKey = "welcome.title", size = size, fullWindowContent = true) { + PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { PDETheme(darkTheme = false) { PDEWelcome() } From e036cdf79d136e402f34cd4ebae5e63ea4936269 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 27 Oct 2025 14:15:54 +0100 Subject: [PATCH 11/23] Localize welcome screen UI strings Replaced hardcoded strings in the PDEWelcome screen with localized values using the LocalLocale context. Added new keys for the welcome screen to the English and Dutch language property files to support internationalization. --- app/src/processing/app/ui/PDEWelcome.kt | 48 ++++++++++++++------ build/shared/lib/languages/PDE.properties | 16 +++++++ build/shared/lib/languages/PDE_nl.properties | 16 +++++++ 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 7a414a3142..662e7268e8 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -77,6 +77,7 @@ import processing.app.Base import processing.app.LocalPreferences import processing.app.Messages import processing.app.Platform +import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.PDEComposeWindow import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme @@ -95,6 +96,7 @@ fun PDEWelcome(base: Base? = null) { .defaultMinSize(minHeight = 1.dp) .height(32.dp) val textColor = if(isSystemInDarkTheme()) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSecondaryContainer + val locale = LocalLocale.current Column( verticalArrangement = Arrangement.SpaceBetween, @@ -116,10 +118,10 @@ fun PDEWelcome(base: Base? = null) { painter = painterResource("logo.svg"), modifier = Modifier .size(75.dp), - contentDescription = "Processing Logo" + contentDescription = locale["welcome.processing.logo"] ) Text( - text = "Welcome to Processing!", + text = locale["welcome.processing.title"], style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), color = textColor, modifier = Modifier @@ -152,7 +154,7 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.NoteAdd, contentDescription = "") Spacer(Modifier.width(12.dp)) - Text("New Empty Sketch") + Text(locale["welcome.actions.sketch.new"]) } TextButton( onClick = { @@ -166,7 +168,7 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.FolderSpecial, contentDescription = "") Spacer(Modifier.width(12.dp)) - Text("Open Examples") + Text(locale["welcome.actions.examples"] ) } } } @@ -188,7 +190,7 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.FolderOpen, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) - Text("Sketchbook", modifier = Modifier.align(Alignment.CenterVertically)) + Text(locale["sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) } TextButton( onClick = { @@ -202,7 +204,7 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.Settings, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) - Text("Settings", modifier = Modifier.align(Alignment.CenterVertically)) + Text(locale["preferences"], modifier = Modifier.align(Alignment.CenterVertically)) } val preferences = LocalPreferences.current val showOnStartup = preferences["welcome.four.show"].toBoolean() @@ -219,7 +221,7 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(if(showOnStartup) Icons.Default.Check else Icons.Default.Close, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) - Text("Show this window on startup", modifier = Modifier.align(Alignment.CenterVertically)) + Text(locale["welcome.actions.show_startup"], modifier = Modifier.align(Alignment.CenterVertically)) } } } @@ -249,7 +251,7 @@ fun PDEWelcome(base: Base? = null) { ProvideTextStyle(MaterialTheme.typography.labelLarge) { Column { Text( - "Resources", + text = locale["welcome.resources.title"], style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), modifier = Modifier.padding(start = 8.dp) ) @@ -263,7 +265,9 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.SmartDisplay, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) - Text("Video Course") + Text( + text = locale["welcome.resources.video"], + ) } TextButton( onClick = { @@ -275,7 +279,9 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.PinDrop, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) - Text("Get Started") + Text( + text = locale["welcome.resources.get_started"], + ) } TextButton( onClick = { @@ -287,7 +293,9 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.School, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) - Text("Tutorials") + Text( + text = locale["welcome.resources.tutorials"], + ) } TextButton( onClick = { @@ -299,12 +307,14 @@ fun PDEWelcome(base: Base? = null) { ) { Icon(Icons.Outlined.Book, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) - Text("Documentation") + Text( + text = locale["welcome.resources.documentation"], + ) } } Column { Text( - "Join our community", + text = locale["welcome.community.title"], style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), modifier = Modifier.padding(start = 8.dp) ) @@ -328,7 +338,9 @@ fun PDEWelcome(base: Base? = null) { modifier = Modifier.size(20.dp) ) Spacer(Modifier.width(4.dp)) - Text("Forum") + Text( + text = locale["welcome.community.forum"] + ) } TextButton( onClick = { @@ -462,6 +474,14 @@ fun PDEWelcome(base: Base? = null) { tint = Color.White ) } + Text( + text = example.path.name, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(8.dp) + ) } } } diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 19a5c9f866..caed0d37d0 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -640,6 +640,22 @@ beta.button = Ok color_chooser = Color Selector color_chooser.select = Select + +# --------------------------------------- +# Welcome Screen +welcome.processing.logo = Processing Logo +welcome.processing.title = Welcome to Processing! +welcome.actions.sketch.new = Empty Sketch +welcome.actions.examples = Open Examples +welcome.actions.show_startup = Show this window at startup +welcome.resources.title = Resources +welcome.resources.video = Video Course +welcome.resources.get_started = Get Started +welcome.resources.tutorials = Tutorials +welcome.resources.documentation = Reference +welcome.community.title = Join our community +welcome.community.forum = Forum + # --------------------------------------- # Movie Maker diff --git a/build/shared/lib/languages/PDE_nl.properties b/build/shared/lib/languages/PDE_nl.properties index e7f11b0a1f..76865397b3 100644 --- a/build/shared/lib/languages/PDE_nl.properties +++ b/build/shared/lib/languages/PDE_nl.properties @@ -322,6 +322,22 @@ beta.title = Dankuwel voor het testen van deze Processing Beta! beta.message = Deze preview release laat ons feedback verzamelen en problemen oplossen. **Sommige functies werken mogelijk niet zoals verwacht.** Als u problemen ondervindt, [post dan op het forum](https://discourse.processing.org) of [open een GitHub issue](https://github.com/processing/processing4/issues). beta.button = Ok + +# --------------------------------------- +# Welcome Screen +welcome.processing.logo = Processing Logo +welcome.processing.title = Welkom bij Processing! +welcome.actions.sketch.new = Nieuwe Schets +welcome.actions.examples = Open Voorbeelden +welcome.actions.show_startup = Laat dit scherm zien bij opstarten +welcome.resources.title = Resources +welcome.resources.video = Video Cursus +welcome.resources.get_started = Om te beginnen +welcome.resources.tutorials = Tutorials +welcome.resources.documentation = Handleiding +welcome.community.title = Neem deel aan de Community +welcome.community.forum = Forum + # --------------------------------------- # Color Chooser color_chooser = Kies een kleur... From d8f9d75c24af363d927c128f3476704caf66572b Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 11:45:14 +0100 Subject: [PATCH 12/23] Add language selector and UI improvements to welcome screen Introduces a language selection dropdown to the PDE welcome screen using a shared composable from preferences. Refactors the layout for better spacing, updates example cards with animated overlays, and replaces the show-on-startup button with a checkbox. Also adds a new translation key for the open example button. --- app/src/processing/app/ui/PDEWelcome.kt | 233 +++++++++++------- .../app/ui/preferences/Interface.kt | 46 ++-- build/shared/lib/languages/PDE.properties | 1 + 3 files changed, 169 insertions(+), 111 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 662e7268e8..0b081afc03 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -1,7 +1,18 @@ package processing.app.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,8 +35,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.ChatBubbleOutline @@ -41,6 +54,8 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -48,6 +63,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -60,6 +76,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.decodeToImageBitmap @@ -71,12 +88,14 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application import processing.app.Base import processing.app.LocalPreferences import processing.app.Messages import processing.app.Platform +import processing.app.ui.preferences.Interface.Companion.languagesDropdown import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.PDEComposeWindow import processing.app.ui.theme.PDESwingWindow @@ -89,7 +108,9 @@ import java.nio.file.Path @Composable fun PDEWelcome(base: Base? = null) { Row( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLow), ){ val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) val xsModifier = Modifier @@ -111,29 +132,49 @@ fun PDEWelcome(base: Base? = null) { ) ) { Row ( - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ){ Image( painter = painterResource("logo.svg"), modifier = Modifier - .size(75.dp), + .size(50.dp), contentDescription = locale["welcome.processing.logo"] ) Text( text = locale["welcome.processing.title"], - style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), color = textColor, modifier = Modifier .align(Alignment.CenterVertically) ) + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.CenterVertically), + horizontalArrangement = Arrangement.End, + ){ + val showLanguageMenu = remember { mutableStateOf(false) } + OutlinedButton( + onClick = { + showLanguageMenu.value = !showLanguageMenu.value + }, + contentPadding = xsPadding, + modifier = xsModifier + ){ + Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text(text = locale.locale.displayName) + Icon(Icons.Default.ArrowDropDown, contentDescription = "", modifier = Modifier.size(20.dp)) + languagesDropdown(showLanguageMenu) + } + + } + } Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() .height(IntrinsicSize.Min) - .padding(vertical = 12.dp) ) { val colors = ButtonDefaults.textButtonColors( contentColor = textColor @@ -144,13 +185,14 @@ fun PDEWelcome(base: Base? = null) { .fillMaxHeight() ) { ProvideTextStyle(MaterialTheme.typography.titleMedium) { + val medModifier = Modifier + .sizeIn(minHeight = 56.dp) TextButton( onClick = { base?.handleNew() ?: noBaseWarning() }, colors = colors, - modifier = Modifier - .sizeIn(minHeight = 56.dp) + modifier = medModifier ) { Icon(Icons.Outlined.NoteAdd, contentDescription = "") Spacer(Modifier.width(12.dp)) @@ -163,66 +205,25 @@ fun PDEWelcome(base: Base? = null) { } ?: noBaseWarning() }, colors = colors, - modifier = Modifier - .sizeIn(minHeight = 56.dp) + modifier = medModifier ) { Icon(Icons.Outlined.FolderSpecial, contentDescription = "") Spacer(Modifier.width(12.dp)) Text(locale["welcome.actions.examples"] ) } - } - } - Column( - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxHeight() - ) { - ProvideTextStyle(MaterialTheme.typography.labelLarge) { TextButton( onClick = { base?.let{ base.showSketchbookFrame() } ?: noBaseWarning() }, - contentPadding = xsPadding, colors = colors, - modifier = xsModifier + modifier = medModifier ) { Icon(Icons.Outlined.FolderOpen, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(12.dp)) Text(locale["sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) } - TextButton( - onClick = { - base?.let{ - base.handlePrefs() - } ?: noBaseWarning() - }, - contentPadding = xsPadding, - colors = colors, - modifier = xsModifier - ) { - Icon(Icons.Outlined.Settings, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(12.dp)) - Text(locale["preferences"], modifier = Modifier.align(Alignment.CenterVertically)) - } - val preferences = LocalPreferences.current - val showOnStartup = preferences["welcome.four.show"].toBoolean() - Button( - onClick = { - preferences["welcome.four.show"] = (!showOnStartup).toString() - }, - contentPadding = xsPadding, - colors = ButtonDefaults.buttonColors( - containerColor = if(showOnStartup) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceContainer, - contentColor = if (showOnStartup) MaterialTheme.colorScheme.onTertiary else MaterialTheme.colorScheme.onSurface - ), - modifier = xsModifier - ) { - Icon(if(showOnStartup) Icons.Default.Check else Icons.Default.Close, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(12.dp)) - Text(locale["welcome.actions.show_startup"], modifier = Modifier.align(Alignment.CenterVertically)) - } } } } @@ -255,20 +256,6 @@ fun PDEWelcome(base: Base? = null) { style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), modifier = Modifier.padding(start = 8.dp) ) - TextButton( - onClick = { - Platform.openURL("https://hello.processing.org") - }, - contentPadding = xsPadding, - modifier = xsModifier, - colors = colors - ) { - Icon(Icons.Outlined.SmartDisplay, contentDescription = "", modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(4.dp)) - Text( - text = locale["welcome.resources.video"], - ) - } TextButton( onClick = { Platform.openURL("https://processing.org/tutorials/gettingstarted") @@ -398,10 +385,35 @@ fun PDEWelcome(base: Base? = null) { } } } + val preferences = LocalPreferences.current + val showOnStartup = preferences["welcome.four.show"].toBoolean() + fun toggle(next: Boolean? = null){ + preferences["welcome.four.show"] = (next ?: !showOnStartup).toString() + } + Row( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = ::toggle) + .padding(end = 8.dp) + .height(32.dp) + ) { + Checkbox( + checked = showOnStartup, + onCheckedChange = ::toggle, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.tertiary + ) + ) + Text( + text = locale["welcome.actions.show_startup"], + modifier = Modifier.align(Alignment.CenterVertically), + style = MaterialTheme.typography.labelLarge + ) + } } - VerticalDivider() Column(modifier = Modifier .sizeIn(minWidth = 350.dp) + .padding(end = 12.dp) ) { val examples = listOf( Example(Platform.getContentFile("modes/java/examples/Basics/Arrays/Array")), @@ -413,12 +425,17 @@ fun PDEWelcome(base: Base? = null) { state = rememberLazyListState( initialFirstVisibleItemScrollOffset = 150 ), - modifier = Modifier.width(350.dp) + modifier = Modifier + .width(350.dp), + contentPadding = PaddingValues(vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(examples) { example -> var hovered by remember { mutableStateOf(false) } Box(Modifier - .background(MaterialTheme.colorScheme.surfaceVariant) + .border(BorderStroke(2.dp, MaterialTheme.colorScheme.outlineVariant), shape = MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceVariant, shape = MaterialTheme.shapes.medium) + .clip(MaterialTheme.shapes.medium) .fillMaxSize() .aspectRatio(16 / 9f) .onPointerEvent(PointerEventType.Enter){ @@ -455,33 +472,65 @@ fun PDEWelcome(base: Base? = null) { contentDescription = example.path.name ) } - if(hovered) { - FilledTonalIconButton( - onClick = { - base?.let { - base.handleOpen(example.path.resolve("${example.path.name}.pde").absolutePath) - } ?: noBaseWarning() - }, modifier = Modifier - .align(Alignment.Center), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary, + Column( + modifier = Modifier.align(Alignment.BottomCenter), + ) { + val duration = 150 + AnimatedVisibility( + visible = hovered, + enter = slideIn( + initialOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = EaseInOut + ) + ), + exit = slideOut ( + targetOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = LinearEasing + ) ) ) { - Icon( - Icons.Filled.PlayArrow, - contentDescription = "Open Example", - tint = Color.White - ) + Card( + modifier = Modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(12.dp) + .padding(start = 12.dp) + ) { + Text( + text = example.path.name, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(8.dp) + ) + Button( + onClick = { + base?.let { + base.handleOpen(example.path.resolve("${example.path.name}.pde").absolutePath) + } ?: noBaseWarning() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary + ) + ) { + Text( + text = locale["welcome.sketch.open"], + style = MaterialTheme.typography.bodyLarge + ) + } + } + } } - Text( - text = example.path.name, - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(8.dp) - ) } } } @@ -501,7 +550,7 @@ fun noBaseWarning() { ) } -val size = DpSize(970.dp, 550.dp) +val size = DpSize(970.dp, 600.dp) val titleKey = "menu.help.welcome" fun showWelcomeScreen(base: Base? = null) { diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt index fc384fbc59..e9747a037d 100644 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -12,6 +12,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,8 +47,7 @@ class Interface { group = interfaceAndFonts, control = { preference, updatePreference -> val locale = LocalLocale.current - var showOptions by remember { mutableStateOf(false) } - val languages = if(Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") + val showOptions = remember { mutableStateOf(false) } TextField( value = locale.locale.displayName, readOnly = true, @@ -57,27 +58,12 @@ class Interface { contentDescription = "Select Font Family", modifier = Modifier .clickable{ - showOptions = true + showOptions.value = true } ) } ) - DropdownMenu( - expanded = showOptions, - onDismissRequest = { - showOptions = false - }, - ) { - languages.forEach { family -> - DropdownMenuItem( - text = { Text(family.value) }, - onClick = { - locale.set(Locale(family.key)) - showOptions = false - } - ) - } - } + languagesDropdown(showOptions) } )) @@ -164,5 +150,27 @@ class Interface { } )) } + + @Composable + fun languagesDropdown(showOptions: MutableState) { + val locale = LocalLocale.current + val languages = if (Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") + DropdownMenu( + expanded = showOptions.value, + onDismissRequest = { + showOptions.value = false + }, + ) { + languages.forEach { family -> + DropdownMenuItem( + text = { Text(family.value) }, + onClick = { + locale.set(Locale(family.key)) + showOptions.value = false + } + ) + } + } + } } } \ No newline at end of file diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index caed0d37d0..c4a96dcd96 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -655,6 +655,7 @@ welcome.resources.tutorials = Tutorials welcome.resources.documentation = Reference welcome.community.title = Join our community welcome.community.forum = Forum +welcome.sketch.open = Open # --------------------------------------- # Movie Maker From 97ab23e8ff166608d1136e0c5856afee1c42295d Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 12:20:45 +0100 Subject: [PATCH 13/23] Refactor example listing and randomize welcome sketches Moved example folder listing logic in Contributions.ExamplesList to a companion object function for reuse. Updated PDEWelcome to display a randomized selection of sketches from all available examples, replacing the previous static list. --- app/src/processing/app/api/Contributions.kt | 210 +++++++++++--------- app/src/processing/app/ui/PDEWelcome.kt | 55 +++-- 2 files changed, 145 insertions(+), 120 deletions(-) diff --git a/app/src/processing/app/api/Contributions.kt b/app/src/processing/app/api/Contributions.kt index 25e693404b..7b35a30593 100644 --- a/app/src/processing/app/api/Contributions.kt +++ b/app/src/processing/app/api/Contributions.kt @@ -28,8 +28,6 @@ class Contributions: SuspendingCliktCommand(){ } class ExamplesList: SuspendingCliktCommand("list") { - - val serializer = Json { prettyPrint = true } @@ -37,107 +35,121 @@ class Contributions: SuspendingCliktCommand(){ override fun help(context: Context) = "List all examples" override suspend fun run() { Platform.init() - // TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now - // TODO: Allow the user to change the sketchbook location - // TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode - val sketchbookFolder = Platform.getDefaultSketchbookFolder() - val resourcesDir = System.getProperty("compose.application.resources.dir") - - val javaMode = "$resourcesDir/modes/java" - - val javaModeExamples = File("$javaMode/examples") - .listFiles() - ?.map { getSketches(it)} - ?: emptyList() - - val javaModeLibrariesExamples = File("$javaMode/libraries") - .listFiles{ it.isDirectory } - ?.map { library -> - val properties = library.resolve("library.properties") - val name = findNameInProperties(properties) ?: library.name - - val libraryExamples = getSketches(library.resolve("examples")) - Sketch.Companion.Folder( - type = "folder", - name = name, - path = library.absolutePath, - mode = "java", - children = libraryExamples?.children ?: emptyList(), - sketches = libraryExamples?.sketches ?: emptyList() - ) - } ?: emptyList() - val javaModeLibraries = Sketch.Companion.Folder( - type = "folder", - name = "Libraries", - path = "$javaMode/libraries", - mode = "java", - children = javaModeLibrariesExamples, - sketches = emptyList() - ) - - val contributedLibraries = sketchbookFolder.resolve("libraries") - .listFiles{ it.isDirectory } - ?.map { library -> - val properties = library.resolve("library.properties") - val name = findNameInProperties(properties) ?: library.name - // Get library name from library.properties if it exists - val libraryExamples = getSketches(library.resolve("examples")) - Sketch.Companion.Folder( - type = "folder", - name = name, - path = library.absolutePath, - mode = "java", - children = libraryExamples?.children ?: emptyList(), - sketches = libraryExamples?.sketches ?: emptyList() - ) - } ?: emptyList() - - val contributedLibrariesFolder = Sketch.Companion.Folder( - type = "folder", - name = "Contributed Libraries", - path = sketchbookFolder.resolve("libraries").absolutePath, - mode = "java", - children = contributedLibraries, - sketches = emptyList() - ) - - val contributedExamples = sketchbookFolder.resolve("examples") - .listFiles{ it.isDirectory } - ?.map { - val properties = it.resolve("examples.properties") - val name = findNameInProperties(properties) ?: it.name - - val sketches = getSketches(it.resolve("examples")) - Sketch.Companion.Folder( - type = "folder", - name, - path = it.absolutePath, - mode = "java", - children = sketches?.children ?: emptyList(), - sketches = sketches?.sketches ?: emptyList(), - ) - } - ?: emptyList() - val contributedExamplesFolder = Sketch.Companion.Folder( - type = "folder", - name = "Contributed Examples", - path = sketchbookFolder.resolve("examples").absolutePath, - mode = "java", - children = contributedExamples, - sketches = emptyList() - ) - - val json = serializer.encodeToString(javaModeExamples + javaModeLibraries + contributedLibrariesFolder + contributedExamplesFolder) + + val json = serializer.encodeToString(listAllExamples()) println(json) } - private fun findNameInProperties(properties: File): String? { - if (!properties.exists()) return null + companion object { + /** + * Get all example sketch folders + * @return List of example sketch folders + */ + fun listAllExamples(): List { + // TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now + // TODO: Allow the user to change the sketchbook location + // TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode + // TODO: Make non-blocking + // TODO: Add tests + + val sketchbookFolder = Platform.getDefaultSketchbookFolder() + val resourcesDir = System.getProperty("compose.application.resources.dir") + + val javaMode = "$resourcesDir/modes/java" + + val javaModeExamples = File("$javaMode/examples") + .listFiles() + ?.map { getSketches(it) } + ?: emptyList() + + val javaModeLibrariesExamples = File("$javaMode/libraries") + .listFiles { it.isDirectory } + ?.map { library -> + val properties = library.resolve("library.properties") + val name = findNameInProperties(properties) ?: library.name + + val libraryExamples = getSketches(library.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name = name, + path = library.absolutePath, + mode = "java", + children = libraryExamples?.children ?: emptyList(), + sketches = libraryExamples?.sketches ?: emptyList() + ) + } ?: emptyList() + val javaModeLibraries = Sketch.Companion.Folder( + type = "folder", + name = "Libraries", + path = "$javaMode/libraries", + mode = "java", + children = javaModeLibrariesExamples, + sketches = emptyList() + ) + + val contributedLibraries = sketchbookFolder.resolve("libraries") + .listFiles { it.isDirectory } + ?.map { library -> + val properties = library.resolve("library.properties") + val name = findNameInProperties(properties) ?: library.name + // Get library name from library.properties if it exists + val libraryExamples = getSketches(library.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name = name, + path = library.absolutePath, + mode = "java", + children = libraryExamples?.children ?: emptyList(), + sketches = libraryExamples?.sketches ?: emptyList() + ) + } ?: emptyList() + + val contributedLibrariesFolder = Sketch.Companion.Folder( + type = "folder", + name = "Contributed Libraries", + path = sketchbookFolder.resolve("libraries").absolutePath, + mode = "java", + children = contributedLibraries, + sketches = emptyList() + ) + + val contributedExamples = sketchbookFolder.resolve("examples") + .listFiles { it.isDirectory } + ?.map { + val properties = it.resolve("examples.properties") + val name = findNameInProperties(properties) ?: it.name + + val sketches = getSketches(it.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name, + path = it.absolutePath, + mode = "java", + children = sketches?.children ?: emptyList(), + sketches = sketches?.sketches ?: emptyList(), + ) + } + ?: emptyList() + val contributedExamplesFolder = Sketch.Companion.Folder( + type = "folder", + name = "Contributed Examples", + path = sketchbookFolder.resolve("examples").absolutePath, + mode = "java", + children = contributedExamples, + sketches = emptyList() + ) + + return javaModeExamples + javaModeLibraries + contributedLibrariesFolder + contributedExamplesFolder + } + + private fun findNameInProperties(properties: File): String? { + if (!properties.exists()) return null - return properties.readLines().firstNotNullOfOrNull { line -> - line.split("=", limit = 2) - .takeIf { it.size == 2 && it[0].trim() == "name" } - ?.let { it[1].trim() } + return properties.readLines().firstNotNullOfOrNull { line -> + line.split("=", limit = 2) + .takeIf { it.size == 2 && it[0].trim() == "name" } + ?.let { it[1].trim() } + } } } } diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 0b081afc03..66f091abee 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -4,8 +4,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.BorderStroke @@ -36,40 +34,30 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Language -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material.icons.outlined.FolderOpen import androidx.compose.material.icons.outlined.FolderSpecial import androidx.compose.material.icons.outlined.NoteAdd import androidx.compose.material.icons.outlined.PinDrop -import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.School -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.SmartDisplay import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonColors -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -77,14 +65,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.decodeToImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent -import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize @@ -95,6 +80,8 @@ import processing.app.Base import processing.app.LocalPreferences import processing.app.Messages import processing.app.Platform +import processing.app.api.Contributions.ExamplesList.Companion.listAllExamples +import processing.app.api.Sketch.Companion.Sketch import processing.app.ui.preferences.Interface.Companion.languagesDropdown import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.PDEComposeWindow @@ -102,7 +89,9 @@ import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme import processing.app.ui.theme.toDimension import java.io.File -import java.nio.file.Path +import kotlin.concurrent.thread +import kotlin.io.path.Path +import kotlin.io.path.exists @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -415,12 +404,37 @@ fun PDEWelcome(base: Base? = null) { .sizeIn(minWidth = 350.dp) .padding(end = 12.dp) ) { - val examples = listOf( + val examples = remember { mutableStateListOf( Example(Platform.getContentFile("modes/java/examples/Basics/Arrays/Array")), Example(Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective")), Example(Platform.getContentFile("modes/java/examples/Basics/Color/Brightness")), Example(Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")), - ) + )} + + remember { + val sketches = mutableListOf() + val sketchFolders = listAllExamples() + fun gatherSketches(folder: processing.app.api.Sketch.Companion.Folder?) { + if (folder == null) return + sketches.addAll(folder.sketches.filter { it -> Path(it.path).resolve("${it.name}.png").exists() }) + folder.children.forEach { child -> + gatherSketches(child) + } + } + sketchFolders.forEach { folder -> + gatherSketches(folder) + } + if(sketches.isEmpty()) { + return@remember + } + + val newExamples = sketches.shuffled().take(20).map { sketch -> + Example(File(sketch.path)) + } + examples.clear() + examples.addAll(newExamples) + } + LazyColumn( state = rememberLazyListState( initialFirstVisibleItemScrollOffset = 150 @@ -467,8 +481,7 @@ fun PDEWelcome(base: Base? = null) { Image( painter = BitmapPainter(imageBitmap), modifier = Modifier -// .fillMaxSize() - , + .fillMaxSize(), contentDescription = example.path.name ) } From 0ee8929a859bb02eebfb38cbe8d512f171755239 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 12:27:59 +0100 Subject: [PATCH 14/23] Refactor example handling to use Sketch objects Replaces Example objects with Sketch objects for managing example sketches in the welcome screen. Updates all relevant usages to reference Sketch properties, simplifying the code and improving clarity. --- app/src/processing/app/ui/PDEWelcome.kt | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 66f091abee..371febc49f 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -405,10 +405,10 @@ fun PDEWelcome(base: Base? = null) { .padding(end = 12.dp) ) { val examples = remember { mutableStateListOf( - Example(Platform.getContentFile("modes/java/examples/Basics/Arrays/Array")), - Example(Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective")), - Example(Platform.getContentFile("modes/java/examples/Basics/Color/Brightness")), - Example(Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")), + Sketch(name = "Array", path = Platform.getContentFile("modes/java/examples/Basics/Arrays/Array").absolutePath), + Sketch(name = "Perspective", path = Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective").absolutePath), + Sketch(name = "Brightness", path = Platform.getContentFile("modes/java/examples/Basics/Color/Brightness").absolutePath), + Sketch(name = "LoadDisplayOBJ", path = Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ").absolutePath), )} remember { @@ -427,12 +427,8 @@ fun PDEWelcome(base: Base? = null) { if(sketches.isEmpty()) { return@remember } - - val newExamples = sketches.shuffled().take(20).map { sketch -> - Example(File(sketch.path)) - } examples.clear() - examples.addAll(newExamples) + examples.addAll(sketches.shuffled().take(20)) } LazyColumn( @@ -460,8 +456,7 @@ fun PDEWelcome(base: Base? = null) { } ){ val image = remember { - val name = example.path.name - File(example.path,"$name.png").takeIf { it.exists() } + File(example.path,"${example.name}.png").takeIf { it.exists() } } if(image == null){ Icon( @@ -482,7 +477,7 @@ fun PDEWelcome(base: Base? = null) { painter = BitmapPainter(imageBitmap), modifier = Modifier .fillMaxSize(), - contentDescription = example.path.name + contentDescription = example.name ) } Column( @@ -519,7 +514,7 @@ fun PDEWelcome(base: Base? = null) { .padding(start = 12.dp) ) { Text( - text = example.path.name, + text = example.name, style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), color = MaterialTheme.colorScheme.onSurface, modifier = Modifier @@ -528,7 +523,7 @@ fun PDEWelcome(base: Base? = null) { Button( onClick = { base?.let { - base.handleOpen(example.path.resolve("${example.path.name}.pde").absolutePath) + base.handleOpen("${example.path}/${example.name}.pde") } ?: noBaseWarning() }, colors = ButtonDefaults.buttonColors( From 8e32abf8ee4c95d096574638a265946494c5050f Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 13:02:00 +0100 Subject: [PATCH 15/23] Add vertical scrollbar to welcome screen examples Introduces a VerticalScrollbar to the examples list in the PDEWelcome screen for improved navigation. Also adjusts spacing and arrangement in several UI components for better layout consistency, and updates the welcome screen title in the language properties. --- app/src/processing/app/ui/PDEWelcome.kt | 231 ++++++++++++---------- build/shared/lib/languages/PDE.properties | 3 +- 2 files changed, 132 insertions(+), 102 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 371febc49f..d848ab3e35 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image +import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -32,6 +33,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Language @@ -209,7 +211,7 @@ fun PDEWelcome(base: Base? = null) { colors = colors, modifier = medModifier ) { - Icon(Icons.Outlined.FolderOpen, contentDescription = "", modifier = Modifier.size(20.dp)) + Icon(Icons.Outlined.FolderOpen, contentDescription = "") Spacer(Modifier.width(12.dp)) Text(locale["sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) } @@ -226,7 +228,7 @@ fun PDEWelcome(base: Base? = null) { ) ){ Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), + horizontalArrangement = Arrangement.spacedBy(48.dp), modifier = Modifier .padding( top = 18.dp, @@ -239,7 +241,9 @@ fun PDEWelcome(base: Base? = null) { contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) ProvideTextStyle(MaterialTheme.typography.labelLarge) { - Column { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( text = locale["welcome.resources.title"], style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), @@ -288,18 +292,22 @@ fun PDEWelcome(base: Base? = null) { ) } } - Column { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( text = locale["welcome.community.title"], style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), modifier = Modifier.padding(start = 8.dp) ) Row( - horizontalArrangement = Arrangement.spacedBy(24.dp), + horizontalArrangement = Arrangement.spacedBy(48.dp), modifier = Modifier .fillMaxWidth() ) { - Column { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { TextButton( onClick = { Platform.openURL("https://discourse.processing.org") @@ -335,7 +343,9 @@ fun PDEWelcome(base: Base? = null) { Text("Discord") } } - Column { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { TextButton( onClick = { Platform.openURL("https://www.instagram.com/processing_core/") @@ -391,7 +401,9 @@ fun PDEWelcome(base: Base? = null) { onCheckedChange = ::toggle, colors = CheckboxDefaults.colors( checkedColor = MaterialTheme.colorScheme.tertiary - ) + ), + modifier = Modifier + .defaultMinSize(minHeight = 1.dp) ) Text( text = locale["welcome.actions.show_startup"], @@ -402,7 +414,6 @@ fun PDEWelcome(base: Base? = null) { } Column(modifier = Modifier .sizeIn(minWidth = 350.dp) - .padding(end = 12.dp) ) { val examples = remember { mutableStateListOf( Sketch(name = "Array", path = Platform.getContentFile("modes/java/examples/Basics/Arrays/Array").absolutePath), @@ -430,111 +441,125 @@ fun PDEWelcome(base: Base? = null) { examples.clear() examples.addAll(sketches.shuffled().take(20)) } - - LazyColumn( - state = rememberLazyListState( - initialFirstVisibleItemScrollOffset = 150 - ), + val state = rememberLazyListState( + initialFirstVisibleItemScrollOffset = 150 + ) + Box( modifier = Modifier - .width(350.dp), - contentPadding = PaddingValues(vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + .width(350.dp) + .padding(end = 4.dp) ) { - items(examples) { example -> - var hovered by remember { mutableStateOf(false) } - Box(Modifier - .border(BorderStroke(2.dp, MaterialTheme.colorScheme.outlineVariant), shape = MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceVariant, shape = MaterialTheme.shapes.medium) - .clip(MaterialTheme.shapes.medium) - .fillMaxSize() - .aspectRatio(16 / 9f) - .onPointerEvent(PointerEventType.Enter){ - hovered = true - } - .onPointerEvent(PointerEventType.Exit){ - hovered = false - } - ){ - val image = remember { - File(example.path,"${example.name}.png").takeIf { it.exists() } - } - if(image == null){ - Icon( - painter = painterResource("logo.svg"), - modifier = Modifier - .size(75.dp) - .align(Alignment.Center) - , - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - contentDescription = "Processing Logo" + LazyColumn( + state = state, + contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp, end = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(examples) { example -> + var hovered by remember { mutableStateOf(false) } + Box( + Modifier + .border( + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + shape = MaterialTheme.shapes.medium ) - HorizontalDivider() - }else { - val imageBitmap: ImageBitmap = remember(image) { - image.inputStream().readAllBytes().decodeToImageBitmap() - } - Image( - painter = BitmapPainter(imageBitmap), - modifier = Modifier - .fillMaxSize(), - contentDescription = example.name + .background( + MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium ) - } - Column( - modifier = Modifier.align(Alignment.BottomCenter), + .clip(MaterialTheme.shapes.medium) + .fillMaxSize() + .aspectRatio(16 / 9f) + .onPointerEvent(PointerEventType.Enter) { + hovered = true + } + .onPointerEvent(PointerEventType.Exit) { + hovered = false + } ) { - val duration = 150 - AnimatedVisibility( - visible = hovered, - enter = slideIn( - initialOffset = { fullSize -> IntOffset(0, fullSize.height) }, - animationSpec = tween( - durationMillis = duration, - easing = EaseInOut - ) - ), - exit = slideOut ( - targetOffset = { fullSize -> IntOffset(0, fullSize.height) }, - animationSpec = tween( - durationMillis = duration, - easing = LinearEasing - ) + val image = remember { + File(example.path, "${example.name}.png").takeIf { it.exists() } + } + if (image == null) { + Icon( + painter = painterResource("logo.svg"), + modifier = Modifier + .size(75.dp) + .align(Alignment.Center), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + contentDescription = "Processing Logo" ) - ) { - Card( + HorizontalDivider() + } else { + val imageBitmap: ImageBitmap = remember(image) { + image.inputStream().readAllBytes().decodeToImageBitmap() + } + Image( + painter = BitmapPainter(imageBitmap), modifier = Modifier + .fillMaxSize(), + contentDescription = example.name + ) + } + Column( + modifier = Modifier.align(Alignment.BottomCenter), + ) { + val duration = 150 + AnimatedVisibility( + visible = hovered, + enter = slideIn( + initialOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = EaseInOut + ) + ), + exit = slideOut( + targetOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = LinearEasing + ) + ) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + Card( modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .padding(12.dp) - .padding(start = 12.dp) ) { - Text( - text = example.name, - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .padding(8.dp) - ) - Button( - onClick = { - base?.let { - base.handleOpen("${example.path}/${example.name}.pde") - } ?: noBaseWarning() - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary - ) + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(12.dp) + .padding(start = 12.dp) ) { Text( - text = locale["welcome.sketch.open"], - style = MaterialTheme.typography.bodyLarge + text = example.name, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(8.dp) ) + Button( + onClick = { + base?.let { + base.handleOpen("${example.path}/${example.name}.pde") + } ?: noBaseWarning() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary + ), + contentPadding = PaddingValues( + horizontal = 12.dp, + vertical = 4.dp + ), + ) { + Text( + text = locale["welcome.sketch.open"], + style = MaterialTheme.typography.bodyLarge + ) + } } } } @@ -542,6 +567,12 @@ fun PDEWelcome(base: Base? = null) { } } } + VerticalScrollbar( + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterEnd), + adapter = rememberScrollbarAdapter(state) + ) } } } diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index c4a96dcd96..8001796f59 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -644,12 +644,11 @@ color_chooser.select = Select # --------------------------------------- # Welcome Screen welcome.processing.logo = Processing Logo -welcome.processing.title = Welcome to Processing! +welcome.processing.title = Welcome to Processing welcome.actions.sketch.new = Empty Sketch welcome.actions.examples = Open Examples welcome.actions.show_startup = Show this window at startup welcome.resources.title = Resources -welcome.resources.video = Video Course welcome.resources.get_started = Get Started welcome.resources.tutorials = Tutorials welcome.resources.documentation = Reference From 2769a07637ac0e66a197e1d941aeae9b6e7716ac Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 13:20:16 +0100 Subject: [PATCH 16/23] Add rounded corners to buttons in PDEWelcome Introduced a RoundedCornerShape with 12.dp radius and applied it to various buttons in the PDEWelcome screen for improved UI consistency and aesthetics. --- app/src/processing/app/ui/PDEWelcome.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index d848ab3e35..9597f1556f 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Language @@ -103,6 +104,7 @@ fun PDEWelcome(base: Base? = null) { .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceContainerLow), ){ + val shape = RoundedCornerShape(12.dp) val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) val xsModifier = Modifier .defaultMinSize(minHeight = 1.dp) @@ -149,7 +151,8 @@ fun PDEWelcome(base: Base? = null) { showLanguageMenu.value = !showLanguageMenu.value }, contentPadding = xsPadding, - modifier = xsModifier + modifier = xsModifier, + shape = shape ){ Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp)) Spacer(Modifier.width(4.dp)) @@ -183,7 +186,8 @@ fun PDEWelcome(base: Base? = null) { base?.handleNew() ?: noBaseWarning() }, colors = colors, - modifier = medModifier + modifier = medModifier, + shape = shape ) { Icon(Icons.Outlined.NoteAdd, contentDescription = "") Spacer(Modifier.width(12.dp)) @@ -196,7 +200,8 @@ fun PDEWelcome(base: Base? = null) { } ?: noBaseWarning() }, colors = colors, - modifier = medModifier + modifier = medModifier, + shape = shape ) { Icon(Icons.Outlined.FolderSpecial, contentDescription = "") Spacer(Modifier.width(12.dp)) @@ -209,7 +214,8 @@ fun PDEWelcome(base: Base? = null) { } ?: noBaseWarning() }, colors = colors, - modifier = medModifier + modifier = medModifier, + shape = shape ) { Icon(Icons.Outlined.FolderOpen, contentDescription = "") Spacer(Modifier.width(12.dp)) From 3b5ea1dd33122853cb61599559e24174e76195f5 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 14:45:42 +0100 Subject: [PATCH 17/23] Refactor PDEWelcome UI and add Sketch card composable Refactored the PDEWelcome screen for improved structure and readability, including extracting the example preview into a reusable Sketch.card composable. Updated icon usage for RTL support, adjusted layout and padding, and improved the examples list initialization. Also, customized scrollbar style in PDETheme for a more consistent UI appearance. --- app/src/processing/app/ui/PDEWelcome.kt | 436 ++++++++++++----------- app/src/processing/app/ui/theme/Theme.kt | 28 +- 2 files changed, 239 insertions(+), 225 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 9597f1556f..e469a9eaff 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -36,13 +36,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.NoteAdd import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material.icons.outlined.FolderOpen import androidx.compose.material.icons.outlined.FolderSpecial -import androidx.compose.material.icons.outlined.NoteAdd import androidx.compose.material.icons.outlined.PinDrop import androidx.compose.material.icons.outlined.School import androidx.compose.material3.Button @@ -87,12 +87,12 @@ import processing.app.api.Contributions.ExamplesList.Companion.listAllExamples import processing.app.api.Sketch.Companion.Sketch import processing.app.ui.preferences.Interface.Companion.languagesDropdown import processing.app.ui.theme.LocalLocale +import processing.app.ui.theme.Locale import processing.app.ui.theme.PDEComposeWindow import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.PDETheme import processing.app.ui.theme.toDimension import java.io.File -import kotlin.concurrent.thread import kotlin.io.path.Path import kotlin.io.path.exists @@ -112,6 +112,9 @@ fun PDEWelcome(base: Base? = null) { val textColor = if(isSystemInDarkTheme()) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSecondaryContainer val locale = LocalLocale.current + /** + * Left main column + */ Column( verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier @@ -124,6 +127,9 @@ fun PDEWelcome(base: Base? = null) { bottom = 56.dp ) ) { + /** + * Title row + */ Row ( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() @@ -142,7 +148,9 @@ fun PDEWelcome(base: Base? = null) { .align(Alignment.CenterVertically) ) Row( - modifier = Modifier.fillMaxWidth().align(Alignment.CenterVertically), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically), horizontalArrangement = Arrangement.End, ){ val showLanguageMenu = remember { mutableStateOf(false) } @@ -160,70 +168,63 @@ fun PDEWelcome(base: Base? = null) { Icon(Icons.Default.ArrowDropDown, contentDescription = "", modifier = Modifier.size(20.dp)) languagesDropdown(showLanguageMenu) } - } - } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - ) { - val colors = ButtonDefaults.textButtonColors( - contentColor = textColor - ) - Column( - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxHeight() - ) { - ProvideTextStyle(MaterialTheme.typography.titleMedium) { - val medModifier = Modifier - .sizeIn(minHeight = 56.dp) - TextButton( - onClick = { - base?.handleNew() ?: noBaseWarning() - }, - colors = colors, - modifier = medModifier, - shape = shape - ) { - Icon(Icons.Outlined.NoteAdd, contentDescription = "") - Spacer(Modifier.width(12.dp)) - Text(locale["welcome.actions.sketch.new"]) - } - TextButton( - onClick = { - base?.let{ - base.showExamplesFrame() - } ?: noBaseWarning() - }, - colors = colors, - modifier = medModifier, - shape = shape - ) { - Icon(Icons.Outlined.FolderSpecial, contentDescription = "") - Spacer(Modifier.width(12.dp)) - Text(locale["welcome.actions.examples"] ) - } - TextButton( - onClick = { - base?.let{ - base.showSketchbookFrame() - } ?: noBaseWarning() - }, - colors = colors, - modifier = medModifier, - shape = shape - ) { - Icon(Icons.Outlined.FolderOpen, contentDescription = "") - Spacer(Modifier.width(12.dp)) - Text(locale["sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) - } + /** + * New sketch, examples, sketchbook card + */ + val colors = ButtonDefaults.textButtonColors( + contentColor = textColor + ) + Column{ + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + val medModifier = Modifier + .sizeIn(minHeight = 56.dp) + TextButton( + onClick = { + base?.handleNew() ?: noBaseWarning() + }, + colors = colors, + modifier = medModifier, + shape = shape + ) { + Icon(Icons.AutoMirrored.Outlined.NoteAdd, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text(locale["welcome.actions.sketch.new"]) + } + TextButton( + onClick = { + base?.let{ + base.showExamplesFrame() + } ?: noBaseWarning() + }, + colors = colors, + modifier = medModifier, + shape = shape + ) { + Icon(Icons.Outlined.FolderSpecial, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text(locale["welcome.actions.examples"] ) + } + TextButton( + onClick = { + base?.let{ + base.showSketchbookFrame() + } ?: noBaseWarning() + }, + colors = colors, + modifier = medModifier, + shape = shape + ) { + Icon(Icons.Outlined.FolderOpen, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text(locale["sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) } } } + /** + * Resources and community card + */ Card( modifier = Modifier .fillMaxWidth() @@ -354,7 +355,7 @@ fun PDEWelcome(base: Base? = null) { ) { TextButton( onClick = { - Platform.openURL("https://www.instagram.com/processing_core/") + Platform.openURL("https://github.com/processing/processing4") }, contentPadding = xsPadding, modifier = xsModifier, @@ -370,7 +371,7 @@ fun PDEWelcome(base: Base? = null) { } TextButton( onClick = { - Platform.openURL("https://github.com/processing/processing4") + Platform.openURL("https://www.instagram.com/processing_core/") }, contentPadding = xsPadding, modifier = xsModifier, @@ -390,42 +391,54 @@ fun PDEWelcome(base: Base? = null) { } } } - val preferences = LocalPreferences.current - val showOnStartup = preferences["welcome.four.show"].toBoolean() - fun toggle(next: Boolean? = null){ - preferences["welcome.four.show"] = (next ?: !showOnStartup).toString() - } - Row( - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .clickable(onClick = ::toggle) - .padding(end = 8.dp) - .height(32.dp) - ) { - Checkbox( - checked = showOnStartup, - onCheckedChange = ::toggle, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.tertiary - ), + /** + * Show on startup checkbox + */ + Row{ + val preferences = LocalPreferences.current + val showOnStartup = preferences["welcome.four.show"].toBoolean() + fun toggle(next: Boolean? = null) { + preferences["welcome.four.show"] = (next ?: !showOnStartup).toString() + } + Row( modifier = Modifier - .defaultMinSize(minHeight = 1.dp) - ) - Text( - text = locale["welcome.actions.show_startup"], - modifier = Modifier.align(Alignment.CenterVertically), - style = MaterialTheme.typography.labelLarge - ) + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = ::toggle) + .padding(end = 8.dp) + .height(32.dp) + ) { + Checkbox( + checked = showOnStartup, + onCheckedChange = ::toggle, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.tertiary + ), + modifier = Modifier + .defaultMinSize(minHeight = 1.dp) + ) + Text( + text = locale["welcome.actions.show_startup"], + modifier = Modifier.align(Alignment.CenterVertically), + style = MaterialTheme.typography.labelLarge + ) + } } } - Column(modifier = Modifier - .sizeIn(minWidth = 350.dp) + /** + * Examples list + */ + val scrollMargin = 35.dp + Column( + modifier = Modifier + .width(350.dp + scrollMargin) ) { val examples = remember { mutableStateListOf( - Sketch(name = "Array", path = Platform.getContentFile("modes/java/examples/Basics/Arrays/Array").absolutePath), - Sketch(name = "Perspective", path = Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective").absolutePath), - Sketch(name = "Brightness", path = Platform.getContentFile("modes/java/examples/Basics/Color/Brightness").absolutePath), - Sketch(name = "LoadDisplayOBJ", path = Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ").absolutePath), + *listOf( + Platform.getContentFile("modes/java/examples/Basics/Arrays/Array"), + Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective"), + Platform.getContentFile("modes/java/examples/Basics/Color/Brightness"), + Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ") + ).map{ Sketch(path = it.absolutePath, name = it.name) }.toTypedArray() )} remember { @@ -452,124 +465,18 @@ fun PDEWelcome(base: Base? = null) { ) Box( modifier = Modifier - .width(350.dp) .padding(end = 4.dp) ) { LazyColumn( state = state, - contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp, end = 20.dp), + contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp, end = 20.dp, start = scrollMargin), verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(examples) { example -> - var hovered by remember { mutableStateOf(false) } - Box( - Modifier - .border( - BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), - shape = MaterialTheme.shapes.medium - ) - .background( - MaterialTheme.colorScheme.surfaceVariant, - shape = MaterialTheme.shapes.medium - ) - .clip(MaterialTheme.shapes.medium) - .fillMaxSize() - .aspectRatio(16 / 9f) - .onPointerEvent(PointerEventType.Enter) { - hovered = true - } - .onPointerEvent(PointerEventType.Exit) { - hovered = false - } - ) { - val image = remember { - File(example.path, "${example.name}.png").takeIf { it.exists() } - } - if (image == null) { - Icon( - painter = painterResource("logo.svg"), - modifier = Modifier - .size(75.dp) - .align(Alignment.Center), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - contentDescription = "Processing Logo" - ) - HorizontalDivider() - } else { - val imageBitmap: ImageBitmap = remember(image) { - image.inputStream().readAllBytes().decodeToImageBitmap() - } - Image( - painter = BitmapPainter(imageBitmap), - modifier = Modifier - .fillMaxSize(), - contentDescription = example.name - ) - } - Column( - modifier = Modifier.align(Alignment.BottomCenter), - ) { - val duration = 150 - AnimatedVisibility( - visible = hovered, - enter = slideIn( - initialOffset = { fullSize -> IntOffset(0, fullSize.height) }, - animationSpec = tween( - durationMillis = duration, - easing = EaseInOut - ) - ), - exit = slideOut( - targetOffset = { fullSize -> IntOffset(0, fullSize.height) }, - animationSpec = tween( - durationMillis = duration, - easing = LinearEasing - ) - ) - ) { - Card( - modifier = Modifier - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .padding(12.dp) - .padding(start = 12.dp) - ) { - Text( - text = example.name, - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .padding(8.dp) - ) - Button( - onClick = { - base?.let { - base.handleOpen("${example.path}/${example.name}.pde") - } ?: noBaseWarning() - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary - ), - contentPadding = PaddingValues( - horizontal = 12.dp, - vertical = 4.dp - ), - ) { - Text( - text = locale["welcome.sketch.open"], - style = MaterialTheme.typography.bodyLarge - ) - } - } - } - } - } + example.card{ + base?.let { + base.handleOpen("${example.path}/${example.name}.pde") + } ?: noBaseWarning() } } } @@ -584,9 +491,118 @@ fun PDEWelcome(base: Base? = null) { } } -data class Example( - val path: File, -) +@Composable +@OptIn(ExperimentalComposeUiApi::class) +fun Sketch.card(onOpen: () -> Unit = {}) { + val locale = LocalLocale.current + val sketch = this + var hovered by remember { mutableStateOf(false) } + Box( + Modifier + .border( + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + shape = MaterialTheme.shapes.medium + ) + .background( + MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ) + .clip(MaterialTheme.shapes.medium) + .fillMaxSize() + .aspectRatio(16 / 9f) + .onPointerEvent(PointerEventType.Enter) { + hovered = true + } + .onPointerEvent(PointerEventType.Exit) { + hovered = false + } + ) { + val image = remember { + File(sketch.path, "${sketch.name}.png").takeIf { it.exists() } + } + if (image == null) { + Icon( + painter = painterResource("logo.svg"), + modifier = Modifier + .size(75.dp) + .align(Alignment.Center), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + contentDescription = "Processing Logo" + ) + HorizontalDivider() + } else { + val imageBitmap: ImageBitmap = remember(image) { + image.inputStream().readAllBytes().decodeToImageBitmap() + } + Image( + painter = BitmapPainter(imageBitmap), + modifier = Modifier + .fillMaxSize(), + contentDescription = sketch.name + ) + } + Column( + modifier = Modifier.align(Alignment.BottomCenter), + ) { + val duration = 150 + AnimatedVisibility( + visible = hovered, + enter = slideIn( + initialOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = EaseInOut + ) + ), + exit = slideOut( + targetOffset = { fullSize -> IntOffset(0, fullSize.height) }, + animationSpec = tween( + durationMillis = duration, + easing = LinearEasing + ) + ) + ) { + Card( + modifier = Modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(12.dp) + .padding(start = 12.dp) + ) { + Text( + text = sketch.name, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(8.dp) + ) + Button( + onClick = onOpen, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onTertiary + ), + contentPadding = PaddingValues( + horizontal = 12.dp, + vertical = 4.dp + ), + ) { + Text( + text = locale["welcome.sketch.open"], + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + } + } + } +} fun noBaseWarning() { Messages.showWarning( @@ -596,7 +612,7 @@ fun noBaseWarning() { } val size = DpSize(970.dp, 600.dp) -val titleKey = "menu.help.welcome" +const val titleKey = "menu.help.welcome" fun showWelcomeScreen(base: Base? = null) { PDESwingWindow(titleKey = titleKey, size = size.toDimension(), fullWindowContent = true) { diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index 7790aba776..cebe2c39de 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -1,6 +1,9 @@ package processing.app.ui.theme +import androidx.compose.foundation.LocalScrollbarStyle +import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.background +import androidx.compose.foundation.defaultScrollbarStyle import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Map @@ -48,6 +52,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.Density @@ -98,6 +103,14 @@ fun PDETheme( ){ Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLowest)) { CompositionLocalProvider( + LocalScrollbarStyle provides ScrollbarStyle( + minimalHeight = 16.dp, + thickness = 8.dp, + shape = MaterialTheme.shapes.extraSmall, + hoverDurationMillis = 300, + unhoverColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + hoverColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f) + ), LocalContentColor provides MaterialTheme.colorScheme.onSurface, // LocalDensity provides Density(1.25f, 1.25f), content = content @@ -161,21 +174,6 @@ fun main() { ) Column { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { -// Button( -// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), -// onClick = {}) { -// Text("Primary", color = MaterialTheme.colorScheme.onPrimary) -// } -// Button( -// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), -// onClick = {}) { -// Text("Secondary", color = MaterialTheme.colorScheme.onSecondary) -// } -// Button( -// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), -// onClick = {}) { -// Text("Tertiary", color = MaterialTheme.colorScheme.onTertiary) -// } val section = colors.subList(0,3) for((name, color, onColor) in section){ Button( From bef00f90459242891da2e0bdf73ded955b88de8d Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 15:05:36 +0100 Subject: [PATCH 18/23] Add unique window handling to prevent duplicates Introduces a 'unique' parameter to PDESwingWindow and PDEComposeWindow, allowing windows to be identified by a KClass and preventing multiple instances of the same window. If a window with the same unique identifier exists, it is brought to the front and the new one is disposed. This helps avoid duplicate welcome or other singleton windows. --- app/src/processing/app/ui/PDEWelcome.kt | 8 +++- app/src/processing/app/ui/theme/Window.kt | 53 +++++++++++++++++------ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index e469a9eaff..d9cae78f15 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -613,9 +613,15 @@ fun noBaseWarning() { val size = DpSize(970.dp, 600.dp) const val titleKey = "menu.help.welcome" +class WelcomeScreen fun showWelcomeScreen(base: Base? = null) { - PDESwingWindow(titleKey = titleKey, size = size.toDimension(), fullWindowContent = true) { + PDESwingWindow( + titleKey = titleKey, + size = size.toDimension(), + unique = WelcomeScreen::class, + fullWindowContent = true + ) { PDEWelcome(base) } } diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index 98a4e00807..dad06087da 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -5,28 +5,24 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposePanel import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.formdev.flatlaf.util.SystemInfo import java.awt.Dimension -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent import javax.swing.JFrame -import javax.swing.UIManager +import kotlin.reflect.KClass val LocalWindow = compositionLocalOf { error("No Window Set") } @@ -48,6 +44,8 @@ val LocalWindow = compositionLocalOf { error("No Window Set") } * @param size The desired size of the window. If null, the window will use its default size. * @param minSize The minimum size of the window. If null, no minimum size is set. * @param maxSize The maximum size of the window. If null, no maximum size is set. + * @param unique An optional unique identifier for the window to prevent duplicates. + * @param onClose A lambda function to be called when the window is requested to close. * @param fullWindowContent If true, the content will extend into the title bar area on macOS. * @param content The composable content to be displayed in the window. */ @@ -56,6 +54,7 @@ class PDESwingWindow( size: Dimension? = null, minSize: Dimension? = null, maxSize: Dimension? = null, + unique: KClass<*>? = null, fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable () -> Unit @@ -75,7 +74,13 @@ class PDESwingWindow( } setLocationRelativeTo(null) setContent { - PDEWindowContent(window, titleKey, fullWindowContent, content) + PDEWindowContent( + window = window, + titleKey = titleKey, + unique = unique, + fullWindowContent = fullWindowContent, + content = content + ) } window.addWindowStateListener { if(it.newState == JFrame.DISPOSE_ON_CLOSE){ @@ -87,12 +92,15 @@ class PDESwingWindow( } } +private val windows = mutableMapOf, ComposeWindow>() + /** * Internal Composable function to set up the window content with theming and localization. * It also handles macOS specific properties for full window content. * * @param window The JFrame instance to be configured. * @param titleKey The key for the window title, which will be localized. + * @param unique An optional unique identifier for the window to prevent duplicates. * @param fullWindowContent If true, the content will extend into the title bar area on macOS. * @param content The composable content to be displayed in the window. */ @@ -100,6 +108,7 @@ class PDESwingWindow( private fun PDEWindowContent( window: ComposeWindow, titleKey: String, + unique: KClass<*>? = null, fullWindowContent: Boolean = false, content: @Composable () -> Unit ){ @@ -108,6 +117,20 @@ private fun PDEWindowContent( window.rootPane.putClientProperty("apple.awt.fullWindowContent", mac && fullWindowContent) window.rootPane.putClientProperty("apple.awt.transparentTitleBar", mac && fullWindowContent) } + if(unique != null && windows.contains(unique) && windows[unique] != null){ + windows[unique]?.toFront() + window.dispose() + return + } + + DisposableEffect(unique){ + unique?.let { + windows[it] = window + } + onDispose { + windows.remove(unique) + } + } CompositionLocalProvider(LocalWindow provides window) { PDETheme{ @@ -148,13 +171,10 @@ private fun PDEWindowContent( * fullscreen if it contains any of [fillMaxWidth]/[fillMaxSize]/[fillMaxHeight] etc. * @param minSize The minimum size of the window. Defaults to unspecified size which means no minimum size is set. * @param maxSize The maximum size of the window. Defaults to unspecified size which means no maximum size is set. - * @param fullWindowContent If true, the content will extend into the title bar area on - * macOS. + * @param unique An optional unique identifier for the window to prevent duplicates. + * @param fullWindowContent If true, the content will extend into the title bar area on macOS. * @param onClose A lambda function to be called when the window is requested to close. * @param content The composable content to be displayed in the window. - * - * - * */ @Composable fun PDEComposeWindow( @@ -162,6 +182,7 @@ fun PDEComposeWindow( size: DpSize = DpSize.Unspecified, minSize: DpSize = DpSize.Unspecified, maxSize: DpSize = DpSize.Unspecified, + unique: KClass<*>? = null, fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable () -> Unit @@ -175,7 +196,13 @@ fun PDEComposeWindow( window.minimumSize = minSize.toDimension() window.maximumSize = maxSize.toDimension() } - PDEWindowContent(window, titleKey, fullWindowContent, content) + PDEWindowContent( + window = window, + titleKey = titleKey, + unique = unique, + fullWindowContent = fullWindowContent, + content = content + ) } } From 07e05be938b3a98f8b5a02b8ce5f5787ef462fc9 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 16:28:30 +0100 Subject: [PATCH 19/23] Refactor dialog handling and improve AlertDialog UI Refactored the showDialog function to accept a modifier and updated all AlertDialog usages to use RectangleShape and the modifier parameter. Improved dialog sizing and positioning by dynamically adjusting the window size based on content, and set additional window properties for better integration on macOS. --- app/src/processing/app/Messages.kt | 52 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/app/src/processing/app/Messages.kt b/app/src/processing/app/Messages.kt index 05bada362c..e8f0979454 100644 --- a/app/src/processing/app/Messages.kt +++ b/app/src/processing/app/Messages.kt @@ -19,23 +19,19 @@ package processing.app import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Alarm import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposeDialog +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -46,14 +42,13 @@ import com.formdev.flatlaf.FlatLightLaf import processing.app.ui.Toolkit import processing.app.ui.theme.PDETheme import java.awt.Dimension -import java.awt.EventQueue import java.awt.Frame import java.io.PrintWriter import java.io.StringWriter -import javax.swing.JFrame import javax.swing.JOptionPane import javax.swing.UIManager + class Messages { companion object { /** @@ -65,9 +60,11 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - showDialog(title) { dismiss -> + showDialog(title) { modifier, dismiss -> AlertDialog( + modifier = modifier, onDismissRequest = { }, + shape = RectangleShape, icon = { Icon(Icons.Default.Info, contentDescription = "Info!") }, title = { Text(title) }, text = { Text(message) }, @@ -96,9 +93,11 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - showDialog(title){ dismiss -> + showDialog(title){ modifier, dismiss -> AlertDialog( + modifier = modifier, onDismissRequest = { }, + shape = RectangleShape, icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, iconContentColor = MaterialTheme.colorScheme.tertiary, title = { Text(title) }, @@ -134,9 +133,11 @@ class Messages { // proper parsing on the command line. Many have \n in them. println("$title: $primary\n$secondary") } else { - showDialog(title){ dismiss -> + showDialog(title){ modifier, dismiss -> AlertDialog( + modifier = modifier, onDismissRequest = { }, + shape = RectangleShape, icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, iconContentColor = MaterialTheme.colorScheme.tertiary, title = { Text(title) }, @@ -173,9 +174,11 @@ class Messages { if (Base.isCommandLine()) { System.err.println("$title: $message") } else { - showDialog(title){ dismiss -> + showDialog(title){ modifier, dismiss -> AlertDialog( + modifier = modifier, onDismissRequest = { }, + shape = RectangleShape, icon = { Icon(Icons.Default.Error, contentDescription = "Alert!") }, iconContentColor = MaterialTheme.colorScheme.error, title = { Text(title) }, @@ -360,17 +363,24 @@ class Messages { } } -fun showDialog(title: String, content: @Composable (dismiss: () -> Unit) -> Unit) { +@OptIn(ExperimentalComposeUiApi::class) +fun showDialog(title: String, content: @Composable (modifier: Modifier, dismiss: () -> Unit) -> Unit) { ComposeDialog().apply { -// isUndecorated = true -// isTransparent = true isModal = true setTitle(title) - size = Dimension(400, 200) + size = Dimension(400, 400) + rootPane.putClientProperty("apple.awt.fullWindowContent", true) + rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + rootPane.putClientProperty("apple.awt.windowTitleVisible", false); + - setContent { + setContent { PDETheme { - content(::dispose) + val density = LocalDensity.current + content(Modifier.onSizeChanged{ + size = Dimension((it.width / density.density).toInt(), (it.height / density.density).toInt()) + setLocationRelativeTo(null) + },::dispose) } } setLocationRelativeTo(null) From 3bdd00ee537aedcc96a8a42368ffebe5123ca2e0 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 20:28:31 +0100 Subject: [PATCH 20/23] Set application window icon using Toolkit.setIcon Added calls to Toolkit.setIcon(window) in Start.kt and Window.kt to ensure the application window icon is set consistent --- app/src/processing/app/ui/Start.kt | 2 ++ app/src/processing/app/ui/theme/Window.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/src/processing/app/ui/Start.kt b/app/src/processing/app/ui/Start.kt index 7de371eec4..d7ed635ecf 100644 --- a/app/src/processing/app/ui/Start.kt +++ b/app/src/processing/app/ui/Start.kt @@ -46,6 +46,8 @@ class Start { var visible by remember { mutableStateOf(false) } val composition = rememberCoroutineScope() LaunchedEffect(Unit) { + Toolkit.setIcon(window) + visible = true composition.launch { delay(duration.toLong() + timeMargin) diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index dad06087da..f725a999b5 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -19,9 +19,11 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.formdev.flatlaf.util.SystemInfo +import processing.app.ui.Toolkit import java.awt.Dimension import javax.swing.JFrame +import javax.swing.JRootPane import kotlin.reflect.KClass val LocalWindow = compositionLocalOf { error("No Window Set") } @@ -116,6 +118,7 @@ private fun PDEWindowContent( remember { window.rootPane.putClientProperty("apple.awt.fullWindowContent", mac && fullWindowContent) window.rootPane.putClientProperty("apple.awt.transparentTitleBar", mac && fullWindowContent) + Toolkit.setIcon(window) } if(unique != null && windows.contains(unique) && windows[unique] != null){ windows[unique]?.toFront() From 65173d47abdb0a05934931d6e755574589382c67 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 29 Oct 2025 07:01:39 +0100 Subject: [PATCH 21/23] Simplify imports and update scrollbar colors in Theme.kt Consolidated import statements for Compose libraries using wildcard imports to reduce verbosity. Updated scrollbar hover and unhover colors to use the default outlineVariant color without alpha modification. --- app/src/processing/app/ui/theme/Theme.kt | 57 +++--------------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index cebe2c39de..c59c5025cd 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -1,61 +1,16 @@ package processing.app.ui.theme -import androidx.compose.foundation.LocalScrollbarStyle -import androidx.compose.foundation.ScrollbarStyle -import androidx.compose.foundation.background -import androidx.compose.foundation.defaultScrollbarStyle -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Map -import androidx.compose.material3.AssistChip -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.Text -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RangeSlider -import androidx.compose.material3.Slider -import androidx.compose.material3.Switch -import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TriStateCheckbox -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Color.Companion import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.state.ToggleableState -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window @@ -108,8 +63,8 @@ fun PDETheme( thickness = 8.dp, shape = MaterialTheme.shapes.extraSmall, hoverDurationMillis = 300, - unhoverColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), - hoverColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f) + unhoverColor = MaterialTheme.colorScheme.outlineVariant, + hoverColor = MaterialTheme.colorScheme.outlineVariant ), LocalContentColor provides MaterialTheme.colorScheme.onSurface, // LocalDensity provides Density(1.25f, 1.25f), From 1143547dd3e2b3d9f275c9585dda3c9cefddf418 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 4 Nov 2025 11:53:57 +0100 Subject: [PATCH 22/23] Removing the Preferences work to keep the PR clean --- app/src/processing/app/Base.java | 36 +- app/src/processing/app/Messages.kt | 146 ++------ app/src/processing/app/ui/PDEWelcome.kt | 89 ++--- app/src/processing/app/ui/Preferences.kt | 323 ------------------ .../processing/app/ui/preferences/General.kt | 121 ------- .../app/ui/preferences/Interface.kt | 176 ---------- .../processing/app/ui/preferences/Other.kt | 73 ---- 7 files changed, 67 insertions(+), 897 deletions(-) delete mode 100644 app/src/processing/app/ui/Preferences.kt delete mode 100644 app/src/processing/app/ui/preferences/General.kt delete mode 100644 app/src/processing/app/ui/preferences/Interface.kt delete mode 100644 app/src/processing/app/ui/preferences/Other.kt diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index fe20b82da1..87bfbd7715 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -23,28 +23,29 @@ package processing.app; -import java.awt.*; -import java.awt.event.ActionListener; -import java.io.*; -import java.lang.reflect.InvocationTargetException; -import java.util.*; -import java.util.List; -import java.util.Map.Entry; - -import javax.swing.*; -import javax.swing.tree.DefaultMutableTreeNode; - import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; import processing.app.contrib.*; import processing.app.tools.Tool; import processing.app.ui.*; -import processing.app.ui.PreferencesKt; import processing.app.ui.Toolkit; -import processing.core.*; +import processing.core.PApplet; import processing.data.StringList; +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import java.awt.*; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.List; +import java.util.Map.Entry; + /** * The base class for the main processing application. * Primary role of this class is for platform identification and @@ -2185,11 +2186,10 @@ static private Mode findSketchMode(File folder, List modeList) { * Show the Preferences window. */ public void handlePrefs() { -// if (preferencesFrame == null) { -// preferencesFrame = new PreferencesFrame(this); -// } -// preferencesFrame.showFrame(); - PreferencesKt.show(); + if (preferencesFrame == null) { + preferencesFrame = new PreferencesFrame(this); + } + preferencesFrame.showFrame(); } diff --git a/app/src/processing/app/Messages.kt b/app/src/processing/app/Messages.kt index e8f0979454..fa9bc54a63 100644 --- a/app/src/processing/app/Messages.kt +++ b/app/src/processing/app/Messages.kt @@ -20,19 +20,9 @@ package processing.app import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposeDialog -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window @@ -41,10 +31,11 @@ import androidx.compose.ui.window.rememberWindowState import com.formdev.flatlaf.FlatLightLaf import processing.app.ui.Toolkit import processing.app.ui.theme.PDETheme -import java.awt.Dimension +import java.awt.EventQueue import java.awt.Frame import java.io.PrintWriter import java.io.StringWriter +import javax.swing.JFrame import javax.swing.JOptionPane import javax.swing.UIManager @@ -60,23 +51,10 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - showDialog(title) { modifier, dismiss -> - AlertDialog( - modifier = modifier, - onDismissRequest = { }, - shape = RectangleShape, - icon = { Icon(Icons.Default.Info, contentDescription = "Info!") }, - title = { Text(title) }, - text = { Text(message) }, - confirmButton = { - Button( - onClick = { dismiss() } - ) { - Text("OK") - } - } - ) - } + JOptionPane.showMessageDialog( + Frame(), message, title, + JOptionPane.INFORMATION_MESSAGE + ) } } @@ -93,27 +71,10 @@ class Messages { if (Base.isCommandLine()) { println("$title: $message") } else { - showDialog(title){ modifier, dismiss -> - AlertDialog( - modifier = modifier, - onDismissRequest = { }, - shape = RectangleShape, - icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, - iconContentColor = MaterialTheme.colorScheme.tertiary, - title = { Text(title) }, - text = { Text(message) }, - confirmButton = { - Button( - onClick = { dismiss() }, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.tertiary - ) - ) { - Text("OK") - } - } - ) - } + JOptionPane.showMessageDialog( + Frame(), message, title, + JOptionPane.WARNING_MESSAGE + ) } e?.printStackTrace() } @@ -133,30 +94,11 @@ class Messages { // proper parsing on the command line. Many have \n in them. println("$title: $primary\n$secondary") } else { - showDialog(title){ modifier, dismiss -> - AlertDialog( - modifier = modifier, - onDismissRequest = { }, - shape = RectangleShape, - icon = { Icon(Icons.Default.Warning, contentDescription = "Alert!") }, - iconContentColor = MaterialTheme.colorScheme.tertiary, - title = { Text(title) }, - text = { - Column { - Text(primary, fontWeight = FontWeight.Bold) - Text(secondary) - } - }, - confirmButton = { - Button( - onClick = { dismiss() }, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.tertiary - ) - ) { - Text("OK") - } - } + EventQueue.invokeLater { + JOptionPane.showMessageDialog( + JFrame(), + Toolkit.formatMessage(primary, secondary), + title, JOptionPane.WARNING_MESSAGE ) } } @@ -174,28 +116,10 @@ class Messages { if (Base.isCommandLine()) { System.err.println("$title: $message") } else { - showDialog(title){ modifier, dismiss -> - AlertDialog( - modifier = modifier, - onDismissRequest = { }, - shape = RectangleShape, - icon = { Icon(Icons.Default.Error, contentDescription = "Alert!") }, - iconContentColor = MaterialTheme.colorScheme.error, - title = { Text(title) }, - text = { Text(message) }, - confirmButton = { - Button( - onClick = { dismiss() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError - ) - ) { - Text("OK") - } - } - ) - } + JOptionPane.showMessageDialog( + Frame(), message, title, + JOptionPane.ERROR_MESSAGE + ) } e?.printStackTrace() System.exit(1) @@ -229,8 +153,6 @@ class Messages { if (fatal) JOptionPane.ERROR_MESSAGE else JOptionPane.WARNING_MESSAGE ) - - if (fatal) { System.exit(1) } @@ -362,32 +284,6 @@ class Messages { } } } - -@OptIn(ExperimentalComposeUiApi::class) -fun showDialog(title: String, content: @Composable (modifier: Modifier, dismiss: () -> Unit) -> Unit) { - ComposeDialog().apply { - isModal = true - setTitle(title) - size = Dimension(400, 400) - rootPane.putClientProperty("apple.awt.fullWindowContent", true) - rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - rootPane.putClientProperty("apple.awt.windowTitleVisible", false); - - - setContent { - PDETheme { - val density = LocalDensity.current - content(Modifier.onSizeChanged{ - size = Dimension((it.width / density.density).toInt(), (it.height / density.density).toInt()) - setLocationRelativeTo(null) - },::dispose) - } - } - setLocationRelativeTo(null) - isVisible = true - } -} - fun main(){ val types = mapOf( "message" to { Messages.showMessage("Test Title", "This is a test message.") }, diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index d9cae78f15..99861d4a59 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -6,64 +6,19 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.NoteAdd import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Language -import androidx.compose.material.icons.outlined.Book -import androidx.compose.material.icons.outlined.ChatBubbleOutline -import androidx.compose.material.icons.outlined.FolderOpen -import androidx.compose.material.icons.outlined.FolderSpecial -import androidx.compose.material.icons.outlined.PinDrop -import androidx.compose.material.icons.outlined.School -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -79,19 +34,10 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application -import processing.app.Base -import processing.app.LocalPreferences -import processing.app.Messages -import processing.app.Platform +import processing.app.* import processing.app.api.Contributions.ExamplesList.Companion.listAllExamples import processing.app.api.Sketch.Companion.Sketch -import processing.app.ui.preferences.Interface.Companion.languagesDropdown -import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.Locale -import processing.app.ui.theme.PDEComposeWindow -import processing.app.ui.theme.PDESwingWindow -import processing.app.ui.theme.PDETheme -import processing.app.ui.theme.toDimension +import processing.app.ui.theme.* import java.io.File import kotlin.io.path.Path import kotlin.io.path.exists @@ -626,6 +572,27 @@ fun showWelcomeScreen(base: Base? = null) { } } +@Composable +fun languagesDropdown(showOptions: MutableState) { + val locale = LocalLocale.current + val languages = if (Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") + DropdownMenu( + expanded = showOptions.value, + onDismissRequest = { + showOptions.value = false + }, + ) { + languages.forEach { family -> + DropdownMenuItem( + text = { Text(family.value) }, + onClick = { + locale.set(java.util.Locale(family.key)) + showOptions.value = false + } + ) + } + } +} fun main(){ application { diff --git a/app/src/processing/app/ui/Preferences.kt b/app/src/processing/app/ui/Preferences.kt deleted file mode 100644 index 12e7c25ce4..0000000000 --- a/app/src/processing/app/ui/Preferences.kt +++ /dev/null @@ -1,323 +0,0 @@ -package processing.app.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPlacement -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.debounce -import processing.app.LocalPreferences -import processing.app.ui.PDEPreferences.Companion.preferences -import processing.app.ui.preferences.General -import processing.app.ui.preferences.Interface -import processing.app.ui.preferences.Other -import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.PDESwingWindow -import processing.app.ui.theme.PDETheme -import java.awt.Dimension -import javax.swing.SwingUtilities - -val LocalPreferenceGroups = compositionLocalOf>> { - error("No Preference Groups Set") -} - -class PDEPreferences { - companion object{ - val groups = mutableStateMapOf>() - fun register(preference: PDEPreference) { - val list = groups[preference.group]?.toMutableList() ?: mutableListOf() - list.add(preference) - groups[preference.group] = list - } - init{ - General.register() - Interface.register() - Other.register() - } - - /** - * Composable function to display the preferences UI. - */ - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun preferences(){ - var visible by remember { mutableStateOf(groups) } - val sortedGroups = remember { - val keys = visible.keys - keys.toSortedSet { - a, b -> - when { - a.after == b -> 1 - b.after == a -> -1 - else -> a.name.compareTo(b.name) - } - } - } - var selected by remember { mutableStateOf(sortedGroups.first()) } - CompositionLocalProvider( - LocalPreferenceGroups provides visible - ) { - Row { - NavigationRail( - header = { - Text( - "Settings", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 42.dp) - ) - }, - modifier = Modifier - .defaultMinSize(minWidth = 200.dp) - ) { - - for (group in sortedGroups) { - NavigationRailItem( - selected = selected == group, - enabled = visible.keys.contains(group), - onClick = { - selected = group - }, - icon = { - group.icon() - }, - label = { - Text(group.name) - } - ) - } - } - Box(modifier = Modifier.padding(top = 42.dp)) { - Column(modifier = Modifier - .fillMaxSize() - ) { - var query by remember { mutableStateOf("") } - val locale = LocalLocale.current - LaunchedEffect(query){ - - snapshotFlow { query } - .debounce(100) - .collect{ - if(it.isBlank()){ - visible = groups - return@collect - } - val filtered = mutableStateMapOf>() - for((group, preferences) in groups){ - val matching = preferences.filter { preference -> - if(preference.key == "other"){ - return@filter true - } - if(preference.key.contains(it, ignoreCase = true)){ - return@filter true - } - val description = locale[preference.descriptionKey] - description.contains(it, ignoreCase = true) - } - if(matching.isNotEmpty()){ - filtered[group] = matching - } - } - visible = filtered - } - - } - SearchBar( - inputField = { - SearchBarDefaults.InputField( - query = query, - onQueryChange = { - query = it - }, - onSearch = { - - }, - expanded = false, - onExpandedChange = { }, - placeholder = { Text("Search") } - ) - }, - expanded = false, - onExpandedChange = {}, - modifier = Modifier.align(Alignment.End).padding(16.dp) - ) { - - } - - val preferences = visible[selected] ?: emptyList() - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - items(preferences){ preference -> - preference.showControl() - } - } - } - } - } - } - } - - - - @JvmStatic - fun main(args: Array) { - application { - Window(onCloseRequest = ::exitApplication){ - remember{ - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } - PDETheme(darkTheme = true) { - preferences() - } - } - Window(onCloseRequest = ::exitApplication){ - remember{ - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } - PDETheme(darkTheme = false) { - preferences() - } - } - } - } - } -} - -/** - * Data class representing a single preference in the preferences system. - * - * Usage: - * ``` - * PDEPreferences.register( - * PDEPreference( - * key = "preference.key", - * descriptionKey = "preference.description", - * group = somePreferenceGroup, - * control = { preference, updatePreference -> - * // Composable UI to modify the preference - * } - * ) - * ) - * ``` - */ -data class PDEPreference( - /** - * The key in the preferences file used to store this preference. - */ - val key: String, - /** - * The key for the description of this preference, used for localization. - */ - val descriptionKey: String, - /** - * The group this preference belongs to. - */ - val group: PDEPreferenceGroup, - /** - * A Composable function that defines the control used to modify this preference. - * It takes the current preference value and a function to update the preference. - */ - val control: @Composable (preference: String?, updatePreference: (newValue: String) -> Unit) -> Unit = { preference, updatePreference -> }, - - /** - * If true, no padding will be applied around this preference's UI. - */ - val noPadding: Boolean = false, -) - -/** - * Composable function to display the preference's description and control. - */ -@Composable -private fun PDEPreference.showControl() { - val locale = LocalLocale.current - val prefs = LocalPreferences.current - Text( - text = locale[descriptionKey], - modifier = Modifier.padding(horizontal = 20.dp), - style = MaterialTheme.typography.titleMedium - ) - val show = @Composable { - control(prefs[key]) { newValue -> - prefs[key] = newValue - } - } - - if(noPadding){ - show() - }else{ - Box(modifier = Modifier.padding(horizontal = 20.dp)) { - show() - } - } -} - -/** - * Data class representing a group of preferences. - */ -data class PDEPreferenceGroup( - /** - * The name of this group. - */ - val name: String, - /** - * The icon representing this group. - */ - val icon: @Composable () -> Unit, - /** - * The group that comes before this one in the list. - */ - val after: PDEPreferenceGroup? = null, -) - -fun show(){ - SwingUtilities.invokeLater { - PDESwingWindow( - titleKey = "preferences", - fullWindowContent = true, - size = Dimension(800, 600) - ) { - PDETheme { - preferences() - } - } - } -} \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt deleted file mode 100644 index 5f56187f46..0000000000 --- a/app/src/processing/app/ui/preferences/General.kt +++ /dev/null @@ -1,121 +0,0 @@ -package processing.app.ui.preferences - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import processing.app.Preferences -import processing.app.SketchName -import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup -import processing.app.ui.PDEPreferences - - -class General { - companion object{ - val general = PDEPreferenceGroup( - name = "General", - icon = { - Icon(Icons.Default.Settings, contentDescription = "A settings icon") - } - ) - - fun register() { - PDEPreferences.register( - PDEPreference( - key = "sketchbook.path.four", - descriptionKey = "preferences.sketchbook_location", - group = general, - control = { preference, updatePreference -> - Row ( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - ) { - TextField( - value = preference ?: "", - onValueChange = { - updatePreference(it) - } - ) - Button( - onClick = { - - } - ) { - Text("Browse") - } - } - } - ) - ) - PDEPreferences.register( - PDEPreference( - key = "sketch.name.approach", - descriptionKey = "preferences.sketch_naming", - group = general, - control = { preference, updatePreference -> - Row{ - for (option in if(Preferences.isInitialized()) SketchName.getOptions() else arrayOf( - "timestamp", - "untitled", - "custom" - )) { - FilterChip( - selected = preference == option, - onClick = { - updatePreference(option) - }, - label = { - Text(option) - }, - modifier = Modifier.padding(4.dp), - ) - } - } - } - ) - ) - PDEPreferences.register( - PDEPreference( - key = "update.check", - descriptionKey = "preferences.check_for_updates_on_startup", - group = general, - control = { preference, updatePreference -> - Switch( - checked = preference.toBoolean(), - onCheckedChange = { - updatePreference(it.toString()) - } - ) - } - ) - ) - PDEPreferences.register( - PDEPreference( - key = "welcome.show", - descriptionKey = "preferences.show_welcome_screen_on_startup", - group = general, - control = { preference, updatePreference -> - Switch( - checked = preference.toBoolean(), - onCheckedChange = { - updatePreference(it.toString()) - } - ) - } - ) - ) - } - } -} \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt deleted file mode 100644 index e9747a037d..0000000000 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ /dev/null @@ -1,176 +0,0 @@ -package processing.app.ui.preferences - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.TextIncrease -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.Slider -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -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.unit.dp -import processing.app.Language -import processing.app.Preferences -import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup -import processing.app.ui.PDEPreferences -import processing.app.ui.Toolkit -import processing.app.ui.preferences.General.Companion.general -import processing.app.ui.theme.LocalLocale -import java.util.Locale - -class Interface { - companion object{ - val interfaceAndFonts = PDEPreferenceGroup( - name = "Interface", - icon = { - Icon(Icons.Default.TextIncrease, contentDescription = "Interface") - }, - after = general - ) - - fun register() { - PDEPreferences.register(PDEPreference( - key = "language", - descriptionKey = "preferences.language", - group = interfaceAndFonts, - control = { preference, updatePreference -> - val locale = LocalLocale.current - val showOptions = remember { mutableStateOf(false) } - TextField( - value = locale.locale.displayName, - readOnly = true, - onValueChange = { }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "Select Font Family", - modifier = Modifier - .clickable{ - showOptions.value = true - } - ) - } - ) - languagesDropdown(showOptions) - } - )) - - PDEPreferences.register( - PDEPreference( - key = "editor.font.family", - descriptionKey = "preferences.editor_and_console_font", - group = interfaceAndFonts, - control = { preference, updatePreference -> - var showOptions by remember { mutableStateOf(false) } - val families = if(Preferences.isInitialized()) Toolkit.getMonoFontFamilies() else arrayOf("Monospaced") - TextField( - value = preference ?: families.firstOrNull().orEmpty(), - readOnly = true, - onValueChange = { updatePreference (it) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "Select Font Family", - modifier = Modifier - .clickable{ - showOptions = true - } - ) - } - ) - DropdownMenu( - expanded = showOptions, - onDismissRequest = { - showOptions = false - }, - ) { - families.forEach { family -> - DropdownMenuItem( - text = { Text(family) }, - onClick = { - updatePreference(family) - showOptions = false - } - ) - } - - } - } - ) - ) - - PDEPreferences.register(PDEPreference( - key = "editor.font.size", - descriptionKey = "preferences.editor_font_size", - group = interfaceAndFonts, - control = { preference, updatePreference -> - Column { - Text( - text = "${preference ?: "12"} pt", - modifier = Modifier.width(120.dp) - ) - Slider( - value = (preference ?: "12").toFloat(), - onValueChange = { updatePreference(it.toInt().toString()) }, - valueRange = 10f..48f, - steps = 18, - ) - } - } - )) - PDEPreferences.register(PDEPreference( - key = "console.font.size", - descriptionKey = "preferences.console_font_size", - group = interfaceAndFonts, - control = { preference, updatePreference -> - Column { - Text( - text = "${preference ?: "12"} pt", - modifier = Modifier.width(120.dp) - ) - Slider( - value = (preference ?: "12").toFloat(), - onValueChange = { updatePreference(it.toInt().toString()) }, - valueRange = 10f..48f, - steps = 18, - ) - } - } - )) - } - - @Composable - fun languagesDropdown(showOptions: MutableState) { - val locale = LocalLocale.current - val languages = if (Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") - DropdownMenu( - expanded = showOptions.value, - onDismissRequest = { - showOptions.value = false - }, - ) { - languages.forEach { family -> - DropdownMenuItem( - text = { Text(family.value) }, - onClick = { - locale.set(Locale(family.key)) - showOptions.value = false - } - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt deleted file mode 100644 index f5f65ea9c8..0000000000 --- a/app/src/processing/app/ui/preferences/Other.kt +++ /dev/null @@ -1,73 +0,0 @@ -package processing.app.ui.preferences - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Map -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import processing.app.LocalPreferences -import processing.app.ui.LocalPreferenceGroups -import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup -import processing.app.ui.PDEPreferences -import processing.app.ui.preferences.Interface.Companion.interfaceAndFonts -import processing.app.ui.theme.LocalLocale - -class Other { - companion object{ - val other = PDEPreferenceGroup( - name = "Other", - icon = { - Icon(Icons.Default.Map, contentDescription = "A map icon") - }, - after = interfaceAndFonts - ) - fun register() { - PDEPreferences.register( - PDEPreference( - key = "other", - descriptionKey = "preferences.other", - group = other, - noPadding = true, - control = { _, _ -> - val prefs = LocalPreferences.current - val groups = LocalPreferenceGroups.current - val restPrefs = remember { - val keys = prefs.keys.mapNotNull { it as? String } - val existing = groups.values.flatten().map { it.key } - keys.filter { it !in existing }.sorted() - } - val locale = LocalLocale.current - - for(prefKey in restPrefs){ - val value = prefs[prefKey] - Row ( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) - ){ - Text( - text = locale[prefKey], - modifier = Modifier.align(Alignment.CenterVertically) - ) - TextField(value ?: "", onValueChange = { - prefs[prefKey] = it - }) - } - } - - } - ) - ) - } - } -} \ No newline at end of file From 0f1173f0178028f7b195e931d02866091046db39 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 6 Nov 2025 08:00:14 +0100 Subject: [PATCH 23/23] Update background color in PDEWelcome UI Changed the background color from surfaceContainerLow to surfaceContainerLowest in the PDEWelcome composable for improved visual consistency with the MaterialTheme. --- app/src/processing/app/ui/PDEWelcome.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 99861d4a59..f097efbb6c 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -48,7 +48,7 @@ fun PDEWelcome(base: Base? = null) { Row( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerLow), + .background(MaterialTheme.colorScheme.surfaceContainerLowest), ){ val shape = RoundedCornerShape(12.dp) val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)