From 4546a3878ace2fca7027b27a14817fc0039b35d6 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 15 Oct 2025 11:39:03 +0200 Subject: [PATCH 01/18] Refactor Locale class and add LocaleProvider test --- app/src/processing/app/ui/theme/Locale.kt | 129 ++++++++++++++++++---- app/test/processing/app/LocaleKtTest.kt | 52 +++++++++ 2 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 app/test/processing/app/LocaleKtTest.kt diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 254c0946c1..0879418a88 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -1,24 +1,41 @@ 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 processing.app.Messages -import processing.app.Platform -import processing.app.PlatformStart -import processing.app.watchFile +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import processing.app.* import java.io.File import java.io.InputStream import java.util.* -class Locale(language: String = "") : Properties() { +/** + * The Locale class extends the standard Java Properties class + * to provide localization capabilities. + * It loads localization resources from property files based on the specified language code. + * The class also provides a method to change the current locale and update the application accordingly. + * Usage: + * ``` + * val locale = Locale("es") { newLocale -> + * // Handle locale change, e.g., update UI or restart application + * } + * val localizedString = locale["someKey"] + * ``` + */ +class Locale(language: String = "", val setLocale: (java.util.Locale) -> Unit) : Properties() { + var locale: java.util.Locale = java.util.Locale.getDefault() + init { - val locale = java.util.Locale.getDefault() - load(ClassLoader.getSystemResourceAsStream("PDE.properties")) - load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream()) - load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream()) - load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream()) + loadResourceUTF8("PDE.properties") + loadResourceUTF8("PDE_${locale.language}.properties") + loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties") + loadResourceUTF8("PDE_${language}.properties") + } + + fun loadResourceUTF8(path: String) { + val stream = ClassLoader.getSystemResourceAsStream(path) + stream?.reader(charset = Charsets.UTF_8)?.use { reader -> + load(reader) + } } @Deprecated("Use get instead", ReplaceWith("get(key)")) @@ -28,18 +45,86 @@ class Locale(language: String = "") : Properties() { return value } operator fun get(key: String): String = getProperty(key, key) + fun set(locale: java.util.Locale) { + setLocale(locale) + } } -val LocalLocale = compositionLocalOf { Locale() } +/** + * A CompositionLocal to provide access to the Locale instance + * throughout the composable hierarchy. see [LocaleProvider] + * Usage: + * ``` + * val locale = LocalLocale.current + * val localizedString = locale["someKey"] + * ``` + */ +val LocalLocale = compositionLocalOf { error("No Locale Set") } + +/** + * This composable function sets up a locale provider that manages application localization. + * It initializes the locale from a language file, watches for changes to that file, and updates + * the locale accordingly. It uses a [Locale] class to handle loading of localized resources. + * + * Usage: + * ``` + * LocaleProvider { + * // Your app content here + * } + * ``` + * + * To access the locale: + * ``` + * val locale = LocalLocale.current + * val localizedString = locale["someKey"] + * ``` + * + * To change the locale: + * ``` + * locale.set(java.util.Locale("es")) + * ``` + * This will update the `language.txt` file and reload the locale. + */ @Composable fun LocaleProvider(content: @Composable () -> Unit) { - PlatformStart() + val preferencesFolderOverride: File? = System.getProperty("processing.app.preferences.folder")?.let { File(it) } + + val settingsFolder = preferencesFolderOverride ?: remember{ + Platform.init() + Platform.getSettingsFolder() + } + val languageFile = settingsFolder.resolve("language.txt") + remember(languageFile){ + if(languageFile.exists()) return@remember - val settingsFolder = Platform.getSettingsFolder() - val languageFile = File(settingsFolder, "language.txt") - watchFile(languageFile) + Messages.log("Creating language file at ${languageFile.absolutePath}") + settingsFolder.mkdirs() + languageFile.writeText(java.util.Locale.getDefault().language) + } + + val update = watchFile(languageFile) + var code by remember(languageFile, update){ mutableStateOf(languageFile.readText().substring(0, 2)) } + remember(code) { + val locale = Locale(code) + java.util.Locale.setDefault(locale) + } + + fun setLocale(locale: java.util.Locale) { + Messages.log("Setting locale to ${locale.language}") + languageFile.writeText(locale.language) + code = locale.language + } + + + val locale = Locale(code, ::setLocale) + remember(code) { Messages.log("Loaded Locale: $code") } + val dir = when(locale["locale.direction"]) { + "rtl" -> LayoutDirection.Rtl + else -> LayoutDirection.Ltr + } - val locale = Locale(languageFile.readText().substring(0, 2)) - CompositionLocalProvider(LocalLocale provides locale) { - content() + CompositionLocalProvider(LocalLayoutDirection provides dir) { + CompositionLocalProvider(LocalLocale provides locale) { + content() + } } } \ No newline at end of file diff --git a/app/test/processing/app/LocaleKtTest.kt b/app/test/processing/app/LocaleKtTest.kt new file mode 100644 index 0000000000..a4e7d637cf --- /dev/null +++ b/app/test/processing/app/LocaleKtTest.kt @@ -0,0 +1,52 @@ +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.ExperimentalTestApi +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import processing.app.ui.theme.LocalLocale +import processing.app.ui.theme.LocaleProvider +import kotlin.io.path.createTempDirectory +import kotlin.test.Test + +class LocaleKtTest { + @OptIn(ExperimentalTestApi::class) + @Test + fun testLocale() = runComposeUiTest { + val tempPreferencesDir = createTempDirectory("preferences") + + System.setProperty("processing.app.preferences.folder", tempPreferencesDir.toFile().absolutePath) + + setContent { + LocaleProvider { + val locale = LocalLocale.current + Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText")) + + Button(onClick = { + locale.setLocale(java.util.Locale("es")) + }, modifier = Modifier.testTag("button")) { + Text("Change") + } + } + } + + // Check if usage generates the language file if it doesn't exist + val languageFile = tempPreferencesDir.resolve("language.txt").toFile() + assert(languageFile.exists()) + + // Check if the text is localised + onNodeWithTag("localisedText").assertTextEquals("New") + + // Change the locale to Spanish + onNodeWithTag("button").performClick() + onNodeWithTag("localisedText").assertTextEquals("Nuevo") + + // Check if the preference was saved to file + assert(languageFile.readText().substring(0, 2) == "es") + } +} \ No newline at end of file From 643ec03090d725ab30de2e57970903cb97969193 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 15 Oct 2025 11:42:36 +0200 Subject: [PATCH 02/18] Make setLocale parameter nullable in Locale class Changed the setLocale parameter in the Locale class to be nullable and updated its usage to safely invoke it. This allows for more flexible instantiation when a setLocale function is not required. --- app/src/processing/app/ui/theme/Locale.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 0879418a88..d760998185 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -21,7 +21,7 @@ import java.util.* * val localizedString = locale["someKey"] * ``` */ -class Locale(language: String = "", val setLocale: (java.util.Locale) -> Unit) : Properties() { +class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() { var locale: java.util.Locale = java.util.Locale.getDefault() init { @@ -46,7 +46,7 @@ class Locale(language: String = "", val setLocale: (java.util.Locale) -> Unit) : } operator fun get(key: String): String = getProperty(key, key) fun set(locale: java.util.Locale) { - setLocale(locale) + setLocale?.invoke(locale) } } /** @@ -104,7 +104,7 @@ fun LocaleProvider(content: @Composable () -> Unit) { val update = watchFile(languageFile) var code by remember(languageFile, update){ mutableStateOf(languageFile.readText().substring(0, 2)) } remember(code) { - val locale = Locale(code) + val locale = java.util.Locale(code) java.util.Locale.setDefault(locale) } From 06e309484065b5cb7f31812c9e569db92f073b7c Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 14 Oct 2025 12:49:25 +0200 Subject: [PATCH 03/18] 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 d42fb2fe365653bea7e052ba2e2bc660719e00c7 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 15 Oct 2025 11:48:00 +0200 Subject: [PATCH 04/18] Update locale change method in test Replaces the call to locale.setLocale with locale.set in LocaleKtTest to match the updated API for changing the locale. --- app/test/processing/app/LocaleKtTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/processing/app/LocaleKtTest.kt b/app/test/processing/app/LocaleKtTest.kt index a4e7d637cf..f8ed32164a 100644 --- a/app/test/processing/app/LocaleKtTest.kt +++ b/app/test/processing/app/LocaleKtTest.kt @@ -28,7 +28,7 @@ class LocaleKtTest { Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText")) Button(onClick = { - locale.setLocale(java.util.Locale("es")) + locale.set(java.util.Locale("es")) }, modifier = Modifier.testTag("button")) { Text("Change") } From 58c746b291b05dc5f2d4ad1a701329a964569d88 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 15 Oct 2025 13:22:18 +0200 Subject: [PATCH 05/18] Add PDE window utilities for Compose and Swing Introduces PDESwingWindow and PDEComposeWindow classes to simplify creating themed and localized windows in Compose and Swing applications. Includes macOS-specific handling for full window content and localization support for window titles. --- app/src/processing/app/ui/theme/Window.kt | 140 ++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 app/src/processing/app/ui/theme/Window.kt diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt new file mode 100644 index 0000000000..6f49843678 --- /dev/null +++ b/app/src/processing/app/ui/theme/Window.kt @@ -0,0 +1,140 @@ +package processing.app.ui.theme + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +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.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import com.formdev.flatlaf.util.SystemInfo + +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.JFrame + +val LocalWindow = compositionLocalOf { error("No Window Set") } + +/** + * A utility class to create a new Window with Compose content in a Swing application. + * It sets up the window with some default properties and allows for custom content. + * Use this when creating a Compose based window from Swing. + * + * Usage example: + * ``` + * SwingUtilities.invokeLater { + * PDESwingWindow("menu.help.welcome", fullWindowContent = true) { + * + * } + * } + * ``` + * + * @param titleKey The key for the window title, which will be localized. + * @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. + */ +class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, content: @Composable BoxScope.() -> Unit): JFrame(){ + init{ + val window = this + defaultCloseOperation = DISPOSE_ON_CLOSE + ComposePanel().apply { + setContent { + PDEWindowContent(window, titleKey, fullWindowContent, content) + } + window.add(this) + } + background = java.awt.Color.white + setLocationRelativeTo(null) + addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + if (e.keyCode == KeyEvent.VK_ESCAPE) window.dispose() + } + }) + isResizable = false + isVisible = true + requestFocus() + } +} + +/** + * 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 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. + */ +@Composable +private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent: Boolean = false, content: @Composable BoxScope.() -> Unit){ + val mac = SystemInfo.isMacOS && SystemInfo.isMacFullWindowContentSupported + remember { + window.rootPane.putClientProperty("apple.awt.fullWindowContent", mac && fullWindowContent) + window.rootPane.putClientProperty("apple.awt.transparentTitleBar", mac && fullWindowContent) + } + + CompositionLocalProvider(LocalWindow provides window) { + ProcessingTheme { + val locale = LocalLocale.current + window.title = locale[titleKey] + LaunchedEffect(locale) { + window.pack() + window.setLocationRelativeTo(null) + } + + Box(modifier = Modifier.padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp),content = content) + } + } +} + +/** + * A Composable function to create and display a new window with the specified content. + * This function sets up the window state and handles the close request. + * Use this when creating a Compose based window from another Compose context. + * + * Usage example: + * ``` + * PDEComposeWindow("window.title", fullWindowContent = true, onClose = { /* handle close */ }) { + * // Your window content here + * Text("Hello, World!") + * } + * ``` + * + * This will create a new window with the title localized from "window.title" key, + * with content extending into the title bar area on macOS, and a custom close handler. + * + * Fully standalone example: + * ``` + * application { + * PDEComposeWindow("window.title", fullWindowContent = true, onClose = ::exitApplication) { + * // Your window content here + * } + * } + * ``` + * + * @param titleKey The key for the window title, which will be localized. + * @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(titleKey: String, fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable BoxScope.() -> Unit){ + val windowState = rememberWindowState( + size = DpSize.Unspecified, + position = WindowPosition(Alignment.Center) + ) + Window(onCloseRequest = onClose, state = windowState, title = "") { + PDEWindowContent(window, titleKey, fullWindowContent, content) + } +} \ No newline at end of file From db69773c43ed84cd24e43baa22279e8ce1125bf2 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 15 Oct 2025 13:36:17 +0200 Subject: [PATCH 06/18] Refactor beta welcome window handling Replaces custom JFrame setup in WelcomeToBeta with PDESwingWindow and PDEComposeWindow, centralizing window logic and close handling. Adds onClose callback to PDESwingWindow for improved lifecycle management. Also ensures beta welcome preference is reset on forced update check. --- app/src/processing/app/ui/Editor.java | 1 + app/src/processing/app/ui/WelcomeToBeta.kt | 57 +++++----------------- app/src/processing/app/ui/theme/Window.kt | 7 ++- 3 files changed, 18 insertions(+), 47 deletions(-) diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index df2440d391..e4b4f15879 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -1057,6 +1057,7 @@ public void buildDevelopMenu(){ var updateTrigger = new JMenuItem(Language.text("menu.develop.check_for_updates")); updateTrigger.addActionListener(e -> { Preferences.unset("update.last"); + Preferences.setInteger("update.beta_welcome", 0); new UpdateCheck(base); }); developMenu.add(updateTrigger); diff --git a/app/src/processing/app/ui/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt index 7757e820f6..ce10fb67cd 100644 --- a/app/src/processing/app/ui/WelcomeToBeta.kt +++ b/app/src/processing/app/ui/WelcomeToBeta.kt @@ -41,6 +41,8 @@ import processing.app.Base.getVersionName import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.LocalTheme import processing.app.ui.theme.Locale +import processing.app.ui.theme.PDEComposeWindow +import processing.app.ui.theme.PDESwingWindow import processing.app.ui.theme.ProcessingTheme import java.awt.Cursor import java.awt.Dimension @@ -54,46 +56,20 @@ import javax.swing.SwingUtilities class WelcomeToBeta { companion object{ - val windowSize = Dimension(400, 200) - val windowTitle = Locale()["beta.window.title"] - @JvmStatic fun showWelcomeToBeta() { - val mac = SystemInfo.isMacFullWindowContentSupported SwingUtilities.invokeLater { - JFrame(windowTitle).apply { - val close = { - Preferences.set("update.beta_welcome", getRevision().toString()) - dispose() - } - rootPane.putClientProperty("apple.awt.transparentTitleBar", mac) - rootPane.putClientProperty("apple.awt.fullWindowContent", mac) - defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE - contentPane.add(ComposePanel().apply { - size = windowSize - setContent { - ProcessingTheme { - Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) { - welcomeToBeta(close) - } - } - } - }) - pack() - background = java.awt.Color.white - setLocationRelativeTo(null) - addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_ESCAPE) close() - } - }) - isResizable = false - isVisible = true - requestFocus() + val close = { + Preferences.set("update.beta_welcome", getRevision().toString()) + } + + PDESwingWindow("beta.window.title", onClose = close) { + welcomeToBeta(close) } } } + val windowSize = Dimension(400, 200) @Composable fun welcomeToBeta(close: () -> Unit = {}) { Row( @@ -194,18 +170,9 @@ class WelcomeToBeta { @JvmStatic fun main(args: Array) { application { - val windowState = rememberWindowState( - size = DpSize.Unspecified, - position = WindowPosition(Alignment.Center) - ) - - Window(onCloseRequest = ::exitApplication, state = windowState, title = windowTitle) { - ProcessingTheme { - Surface(color = colors.background) { - welcomeToBeta { - exitApplication() - } - } + PDEComposeWindow(titleKey = "beta.window.title", onClose = ::exitApplication){ + welcomeToBeta { + exitApplication() } } } diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index 6f49843678..91d245089e 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -40,7 +40,7 @@ val LocalWindow = compositionLocalOf { error("No Window Set") } * @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. */ -class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, content: @Composable BoxScope.() -> Unit): JFrame(){ +class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable BoxScope.() -> Unit): JFrame(){ init{ val window = this defaultCloseOperation = DISPOSE_ON_CLOSE @@ -54,7 +54,10 @@ class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, setLocationRelativeTo(null) addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_ESCAPE) window.dispose() + if (e.keyCode != KeyEvent.VK_ESCAPE) return + + window.dispose() + onClose() } }) isResizable = false From d3681f38c63d2353334c41994473911521827afd Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 16 Oct 2025 18:24:51 +0200 Subject: [PATCH 07/18] Remove ContributionManager and ContributionPane UI files (#1276) 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 bf4d163c2eca0edd5fd3f3ee532275e2c8a73c3d Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Fri, 17 Oct 2025 03:56:35 +0200 Subject: [PATCH 08/18] Refactor Locale class and add LocaleProvider test (#1283) * Refactor Locale class and add LocaleProvider test * Make setLocale parameter nullable in Locale class Changed the setLocale parameter in the Locale class to be nullable and updated its usage to safely invoke it. This allows for more flexible instantiation when a setLocale function is not required. * Add compose ui test to the deps * Update locale change method in test Replaces the call to locale.setLocale with locale.set in LocaleKtTest to match the updated API for changing the locale. --- app/build.gradle.kts | 3 + app/src/processing/app/ui/theme/Locale.kt | 129 ++++++++++++++++++---- app/test/processing/app/LocaleKtTest.kt | 52 +++++++++ 3 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 app/test/processing/app/LocaleKtTest.kt 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) diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 254c0946c1..d760998185 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -1,24 +1,41 @@ 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 processing.app.Messages -import processing.app.Platform -import processing.app.PlatformStart -import processing.app.watchFile +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import processing.app.* import java.io.File import java.io.InputStream import java.util.* -class Locale(language: String = "") : Properties() { +/** + * The Locale class extends the standard Java Properties class + * to provide localization capabilities. + * It loads localization resources from property files based on the specified language code. + * The class also provides a method to change the current locale and update the application accordingly. + * Usage: + * ``` + * val locale = Locale("es") { newLocale -> + * // Handle locale change, e.g., update UI or restart application + * } + * val localizedString = locale["someKey"] + * ``` + */ +class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() { + var locale: java.util.Locale = java.util.Locale.getDefault() + init { - val locale = java.util.Locale.getDefault() - load(ClassLoader.getSystemResourceAsStream("PDE.properties")) - load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream()) - load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream()) - load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream()) + loadResourceUTF8("PDE.properties") + loadResourceUTF8("PDE_${locale.language}.properties") + loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties") + loadResourceUTF8("PDE_${language}.properties") + } + + fun loadResourceUTF8(path: String) { + val stream = ClassLoader.getSystemResourceAsStream(path) + stream?.reader(charset = Charsets.UTF_8)?.use { reader -> + load(reader) + } } @Deprecated("Use get instead", ReplaceWith("get(key)")) @@ -28,18 +45,86 @@ class Locale(language: String = "") : Properties() { return value } operator fun get(key: String): String = getProperty(key, key) + fun set(locale: java.util.Locale) { + setLocale?.invoke(locale) + } } -val LocalLocale = compositionLocalOf { Locale() } +/** + * A CompositionLocal to provide access to the Locale instance + * throughout the composable hierarchy. see [LocaleProvider] + * Usage: + * ``` + * val locale = LocalLocale.current + * val localizedString = locale["someKey"] + * ``` + */ +val LocalLocale = compositionLocalOf { error("No Locale Set") } + +/** + * This composable function sets up a locale provider that manages application localization. + * It initializes the locale from a language file, watches for changes to that file, and updates + * the locale accordingly. It uses a [Locale] class to handle loading of localized resources. + * + * Usage: + * ``` + * LocaleProvider { + * // Your app content here + * } + * ``` + * + * To access the locale: + * ``` + * val locale = LocalLocale.current + * val localizedString = locale["someKey"] + * ``` + * + * To change the locale: + * ``` + * locale.set(java.util.Locale("es")) + * ``` + * This will update the `language.txt` file and reload the locale. + */ @Composable fun LocaleProvider(content: @Composable () -> Unit) { - PlatformStart() + val preferencesFolderOverride: File? = System.getProperty("processing.app.preferences.folder")?.let { File(it) } + + val settingsFolder = preferencesFolderOverride ?: remember{ + Platform.init() + Platform.getSettingsFolder() + } + val languageFile = settingsFolder.resolve("language.txt") + remember(languageFile){ + if(languageFile.exists()) return@remember - val settingsFolder = Platform.getSettingsFolder() - val languageFile = File(settingsFolder, "language.txt") - watchFile(languageFile) + Messages.log("Creating language file at ${languageFile.absolutePath}") + settingsFolder.mkdirs() + languageFile.writeText(java.util.Locale.getDefault().language) + } + + val update = watchFile(languageFile) + var code by remember(languageFile, update){ mutableStateOf(languageFile.readText().substring(0, 2)) } + remember(code) { + val locale = java.util.Locale(code) + java.util.Locale.setDefault(locale) + } + + fun setLocale(locale: java.util.Locale) { + Messages.log("Setting locale to ${locale.language}") + languageFile.writeText(locale.language) + code = locale.language + } + + + val locale = Locale(code, ::setLocale) + remember(code) { Messages.log("Loaded Locale: $code") } + val dir = when(locale["locale.direction"]) { + "rtl" -> LayoutDirection.Rtl + else -> LayoutDirection.Ltr + } - val locale = Locale(languageFile.readText().substring(0, 2)) - CompositionLocalProvider(LocalLocale provides locale) { - content() + CompositionLocalProvider(LocalLayoutDirection provides dir) { + CompositionLocalProvider(LocalLocale provides locale) { + content() + } } } \ No newline at end of file diff --git a/app/test/processing/app/LocaleKtTest.kt b/app/test/processing/app/LocaleKtTest.kt new file mode 100644 index 0000000000..f8ed32164a --- /dev/null +++ b/app/test/processing/app/LocaleKtTest.kt @@ -0,0 +1,52 @@ +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.ExperimentalTestApi +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import processing.app.ui.theme.LocalLocale +import processing.app.ui.theme.LocaleProvider +import kotlin.io.path.createTempDirectory +import kotlin.test.Test + +class LocaleKtTest { + @OptIn(ExperimentalTestApi::class) + @Test + fun testLocale() = runComposeUiTest { + val tempPreferencesDir = createTempDirectory("preferences") + + System.setProperty("processing.app.preferences.folder", tempPreferencesDir.toFile().absolutePath) + + setContent { + LocaleProvider { + val locale = LocalLocale.current + Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText")) + + Button(onClick = { + locale.set(java.util.Locale("es")) + }, modifier = Modifier.testTag("button")) { + Text("Change") + } + } + } + + // Check if usage generates the language file if it doesn't exist + val languageFile = tempPreferencesDir.resolve("language.txt").toFile() + assert(languageFile.exists()) + + // Check if the text is localised + onNodeWithTag("localisedText").assertTextEquals("New") + + // Change the locale to Spanish + onNodeWithTag("button").performClick() + onNodeWithTag("localisedText").assertTextEquals("Nuevo") + + // Check if the preference was saved to file + assert(languageFile.readText().substring(0, 2) == "es") + } +} \ No newline at end of file From 77eba30bb2ca8dbfcba148761a3f6b3fc7904e36 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 22 Oct 2025 04:46:57 +0200 Subject: [PATCH 09/18] Theming (#1298) * Add Material3-based Processing theme and typography Introduces Colors.kt with custom color schemes for light and dark themes using Material3. Refactors Theme.kt to use Material3 theming, adds a PDETheme composable, and provides a desktop preview app for theme components. Updates Typography.kt to use Space Grotesk font family and defines new typography styles for Material3. * Refactor to use Material3 and update theme usage Replaces Material2 components with Material3 in WelcomeToBeta, removes custom PDEButton in favor of Material3 Button, and updates theme usage to PDETheme. Also simplifies background modifier in PDETheme and removes unused Kotlin Multiplatform plugin from build.gradle.kts. * Add Space Grotesk font files and license Includes SpaceGrotesk font variants (Bold, Light, Medium, Regular, SemiBold) and the associated SIL Open Font License. This enables usage of the Space Grotesk typeface in the project. * Update markdown renderer to m3 and adjust UI Switched markdown renderer imports from m2 to m3 and updated the dependency version to 0.37.0. Adjusted WelcomeToBeta window size, layout, and logo dimensions for improved appearance. Ensured Box in Theme.kt fills available space for better layout consistency. --- app/build.gradle.kts | 13 +- app/src/processing/app/ui/WelcomeToBeta.kt | 85 +--- app/src/processing/app/ui/theme/Colors.kt | 134 ++++++ app/src/processing/app/ui/theme/Theme.kt | 399 +++++++++++++++--- app/src/processing/app/ui/theme/Typography.kt | 101 ++++- build.gradle.kts | 1 - build/shared/lib/fonts/SpaceGrotesk-Bold.ttf | Bin 0 -> 86520 bytes .../shared/lib/fonts/SpaceGrotesk-LICENSE.txt | 93 ++++ build/shared/lib/fonts/SpaceGrotesk-Light.ttf | Bin 0 -> 86616 bytes .../shared/lib/fonts/SpaceGrotesk-Medium.ttf | Bin 0 -> 86616 bytes .../shared/lib/fonts/SpaceGrotesk-Regular.ttf | Bin 0 -> 86592 bytes .../lib/fonts/SpaceGrotesk-SemiBold.ttf | Bin 0 -> 86576 bytes gradle/libs.versions.toml | 11 +- 13 files changed, 700 insertions(+), 137 deletions(-) create mode 100644 app/src/processing/app/ui/theme/Colors.kt create mode 100644 build/shared/lib/fonts/SpaceGrotesk-Bold.ttf create mode 100644 build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt create mode 100644 build/shared/lib/fonts/SpaceGrotesk-Light.ttf create mode 100644 build/shared/lib/fonts/SpaceGrotesk-Medium.ttf create mode 100644 build/shared/lib/fonts/SpaceGrotesk-Regular.ttf create mode 100644 build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1aea9ac6b2..6c8ac55f00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,6 +17,7 @@ plugins{ alias(libs.plugins.compose.compiler) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.serialization) alias(libs.plugins.download) } @@ -60,7 +61,7 @@ compose.desktop { ).map { "-D${it.first}=${it.second}" }.toTypedArray()) nativeDistributions{ - modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting", "jdk.httpserver") + modules("jdk.jdi", "java.compiler", "jdk.accessibility", "jdk.zipfs", "java.management.rmi", "java.scripting", "jdk.httpserver") targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Processing" @@ -108,27 +109,29 @@ dependencies { implementation(compose.runtime) implementation(compose.foundation) - implementation(compose.material) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(compose.materialIconsExtended) implementation(compose.desktop.currentOs) + implementation(libs.material3) implementation(libs.compottie) implementation(libs.kaml) implementation(libs.markdown) implementation(libs.markdownJVM) + implementation(libs.clikt) + implementation(libs.kotlinxSerializationJson) + @OptIn(ExperimentalComposeLibrary::class) testImplementation(compose.uiTest) testImplementation(kotlin("test")) testImplementation(libs.mockitoKotlin) testImplementation(libs.junitJupiter) testImplementation(libs.junitJupiterParams) - - implementation(libs.clikt) - implementation(libs.kotlinxSerializationJson) + } tasks.test { diff --git a/app/src/processing/app/ui/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt index 7757e820f6..2725a78176 100644 --- a/app/src/processing/app/ui/WelcomeToBeta.kt +++ b/app/src/processing/app/ui/WelcomeToBeta.kt @@ -5,11 +5,11 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.MaterialTheme.colors -import androidx.compose.material.MaterialTheme.typography -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -31,17 +31,16 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.formdev.flatlaf.util.SystemInfo import com.mikepenz.markdown.compose.Markdown -import com.mikepenz.markdown.m2.markdownColor -import com.mikepenz.markdown.m2.markdownTypography +import com.mikepenz.markdown.m3.markdownColor +import com.mikepenz.markdown.m3.markdownTypography import com.mikepenz.markdown.model.MarkdownColors import com.mikepenz.markdown.model.MarkdownTypography import processing.app.Preferences import processing.app.Base.getRevision import processing.app.Base.getVersionName import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.LocalTheme import processing.app.ui.theme.Locale -import processing.app.ui.theme.ProcessingTheme +import processing.app.ui.theme.PDETheme import java.awt.Cursor import java.awt.Dimension import java.awt.event.KeyAdapter @@ -54,7 +53,7 @@ import javax.swing.SwingUtilities class WelcomeToBeta { companion object{ - val windowSize = Dimension(400, 200) + val windowSize = Dimension(400, 250) val windowTitle = Locale()["beta.window.title"] @JvmStatic @@ -72,7 +71,7 @@ class WelcomeToBeta { contentPane.add(ComposePanel().apply { size = windowSize setContent { - ProcessingTheme { + PDETheme(darkTheme = false) { Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) { welcomeToBeta(close) } @@ -99,7 +98,7 @@ class WelcomeToBeta { Row( modifier = Modifier .padding(20.dp, 10.dp) - .size(windowSize.width.dp, windowSize.height.dp), + .fillMaxSize(), horizontalArrangement = Arrangement .spacedBy(20.dp) ){ @@ -109,7 +108,7 @@ class WelcomeToBeta { contentDescription = locale["beta.logo"], modifier = Modifier .align(Alignment.CenterVertically) - .size(100.dp, 100.dp) + .size(120.dp) .offset(0.dp, (-25).dp) ) Column( @@ -123,7 +122,7 @@ class WelcomeToBeta { ) { Text( text = locale["beta.title"], - style = typography.subtitle1, + style = typography.titleLarge, ) val text = locale["beta.message"] .replace('$' + "version", getVersionName()) @@ -131,80 +130,36 @@ class WelcomeToBeta { Markdown( text, colors = markdownColor(), - typography = markdownTypography(text = typography.body1, link = typography.body1.copy(color = colors.primary)), + typography = markdownTypography(), modifier = Modifier.background(Color.Transparent).padding(bottom = 10.dp) ) Row { Spacer(modifier = Modifier.weight(1f)) - PDEButton(onClick = { + Button(onClick = { close() }) { Text( text = locale["beta.button"], - color = colors.onPrimary + color = MaterialTheme.colorScheme.onPrimary ) } } } } } - @OptIn(ExperimentalComposeUiApi::class) - @Composable - fun PDEButton(onClick: () -> Unit, content: @Composable BoxScope.() -> Unit) { - val theme = LocalTheme.current - - var hover by remember { mutableStateOf(false) } - var clicked by remember { mutableStateOf(false) } - val offset by animateFloatAsState(if (hover) -5f else 5f) - val color by animateColorAsState(if(clicked) colors.primaryVariant else colors.primary) - - Box(modifier = Modifier.padding(end = 5.dp, top = 5.dp)) { - Box( - modifier = Modifier - .offset((-offset).dp, (offset).dp) - .background(theme.getColor("toolbar.button.pressed.field")) - .matchParentSize() - ) - Box( - modifier = Modifier - .onPointerEvent(PointerEventType.Press) { - clicked = true - } - .onPointerEvent(PointerEventType.Release) { - clicked = false - onClick() - } - .onPointerEvent(PointerEventType.Enter) { - hover = true - } - .onPointerEvent(PointerEventType.Exit) { - hover = false - } - .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))) - .background(color) - .padding(10.dp) - .sizeIn(minWidth = 100.dp), - contentAlignment = Alignment.Center, - content = content - ) - } - } - @JvmStatic fun main(args: Array) { application { val windowState = rememberWindowState( - size = DpSize.Unspecified, + size = windowSize.let { DpSize(it.width.dp, it.height.dp) }, position = WindowPosition(Alignment.Center) ) Window(onCloseRequest = ::exitApplication, state = windowState, title = windowTitle) { - ProcessingTheme { - Surface(color = colors.background) { - welcomeToBeta { - exitApplication() - } + PDETheme(darkTheme = false) { + welcomeToBeta { + exitApplication() } } } diff --git a/app/src/processing/app/ui/theme/Colors.kt b/app/src/processing/app/ui/theme/Colors.kt new file mode 100644 index 0000000000..61c6d6b55f --- /dev/null +++ b/app/src/processing/app/ui/theme/Colors.kt @@ -0,0 +1,134 @@ +package processing.app.ui.theme + +import androidx.compose.material.Colors +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +class ProcessingColors{ + companion object{ + val blue = Color(0xFF0251c8) + val lightBlue = Color(0xFF82AFFF) + + val deepBlue = Color(0xFF1e32aa) + val darkBlue = Color(0xFF0F195A) + + val white = Color(0xFFFFFFFF) + val lightGray = Color(0xFFF5F5F5) + val gray = Color(0xFFDBDBDB) + val darkGray = Color(0xFF898989) + val darkerGray = Color(0xFF727070) + val veryDarkGray = Color(0xFF1E1E1E) + val black = Color(0xFF0D0D0D) + + val error = Color(0xFFFF5757) + val errorContainer = Color(0xFFFFA6A6) + + val p5Light = Color(0xFFfd9db9) + val p5Mid = Color(0xFFff4077) + val p5Dark = Color(0xFFaf1f42) + + val foundationLight = Color(0xFFd4b2fe) + val foundationMid = Color(0xFF9c4bff) + val foundationDark = Color(0xFF5501a4) + + val downloadInactive = Color(0xFF8890B3) + val downloadBackgroundActive = Color(0x14508BFF) + } +} + +@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, + + primaryContainer = ProcessingColors.downloadBackgroundActive, + onPrimaryContainer = ProcessingColors.darkBlue, + + secondary = ProcessingColors.deepBlue, + onSecondary = ProcessingColors.white, + + secondaryContainer = ProcessingColors.downloadInactive, + onSecondaryContainer = ProcessingColors.white, + + tertiary = ProcessingColors.p5Mid, + onTertiary = ProcessingColors.white, + + tertiaryContainer = ProcessingColors.p5Light, + onTertiaryContainer = ProcessingColors.p5Dark, + + background = ProcessingColors.white, + onBackground = ProcessingColors.darkBlue, + + surface = ProcessingColors.lightGray, + onSurface = ProcessingColors.darkerGray, + + error = ProcessingColors.error, + onError = ProcessingColors.white, + + errorContainer = ProcessingColors.errorContainer, + onErrorContainer = ProcessingColors.white +) + +val PDEDarkColor = darkColorScheme( + primary = ProcessingColors.deepBlue, + onPrimary = ProcessingColors.white, + + secondary = ProcessingColors.lightBlue, + onSecondary = ProcessingColors.white, + + tertiary = ProcessingColors.blue, + onTertiary = ProcessingColors.white, + + background = ProcessingColors.veryDarkGray, + onBackground = ProcessingColors.white, + + surface = ProcessingColors.darkerGray, + onSurface = ProcessingColors.lightGray, + + error = ProcessingColors.error, + onError = ProcessingColors.white, +) \ 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 735d8e5b2a..7cc70455f0 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -1,75 +1,364 @@ package processing.app.ui.theme +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme +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.verticalScroll +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.compositionLocalOf -import androidx.compose.ui.graphics.Color -import processing.app.LocalPreferences +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.Modifier +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 +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import processing.app.PreferencesProvider -import java.io.InputStream -import java.util.Properties - - -class Theme(themeFile: String? = "") : Properties() { - init { - load(ClassLoader.getSystemResourceAsStream("theme.txt")) - load(ClassLoader.getSystemResourceAsStream(themeFile) ?: InputStream.nullInputStream()) - } - fun getColor(key: String): Color { - return Color(getProperty(key).toColorInt()) - } -} - -val LocalTheme = compositionLocalOf { error("No theme provided") } +/** + * Processing Theme for Jetpack Compose Desktop + * Based on Material3 + * + * Makes Material3 components follow Processing color scheme and typography + * We experimented with using the material3 theme builder, but it made it look too Android-y + * So we defined our own color scheme and typography based on Processing design guidelines + * + * This composable also provides Preferences and Locale context to all child composables + * + * Also, important: sets a default density of 1.25 for better scaling on desktop screens, [LocalDensity] + * + * Usage: + * ``` + * PDETheme { + * val pref = LocalPreferences.current + * val locale = LocalLocale.current + * ... + * // Your composables here + * } + * ``` + * + * @param darkTheme Whether to use dark theme or light theme. Defaults to system setting. + */ @Composable -fun ProcessingTheme( +fun PDETheme( darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit -) { + content: @Composable () -> Unit +){ PreferencesProvider { - val preferences = LocalPreferences.current - val theme = Theme(preferences.getProperty("theme")) - val colors = Colors( - primary = theme.getColor("editor.gradient.top"), - primaryVariant = theme.getColor("toolbar.button.pressed.field"), - secondary = theme.getColor("editor.gradient.bottom"), - secondaryVariant = theme.getColor("editor.scrollbar.thumb.pressed.color"), - background = theme.getColor("editor.bgcolor"), - surface = theme.getColor("editor.bgcolor"), - error = theme.getColor("status.error.bgcolor"), - onPrimary = theme.getColor("toolbar.button.enabled.field"), - onSecondary = theme.getColor("toolbar.button.enabled.field"), - onBackground = theme.getColor("editor.fgcolor"), - onSurface = theme.getColor("editor.fgcolor"), - onError = theme.getColor("status.error.fgcolor"), - isLight = theme.getProperty("laf.mode").equals("light") + LocaleProvider { + MaterialTheme( + colorScheme = if(darkTheme) PDEDarkColor else PDELightColor, + typography = PDETypography + ){ + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background).fillMaxSize()) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + LocalDensity provides Density(1.25f, 1.25f), + content = content + ) + } + } + } + } +} + +/** + * Simple app to preview the Processing Theme components + * Includes buttons, text fields, checkboxes, sliders, etc. + * Run by executing the main() function by clicking the green arrow next to it in intelliJ IDEA + */ +fun main() { + application { + val windowState = rememberWindowState( + size = DpSize(800.dp, 600.dp), + position = WindowPosition(Alignment.Center) ) + var darkTheme by remember { mutableStateOf(false) } + Window(onCloseRequest = ::exitApplication, state = windowState, title = "Processing Theme") { + PDETheme(darkTheme = darkTheme) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Processing Theme Components", style = MaterialTheme.typography.titleLarge) + Card { + Row { + Checkbox(darkTheme, onCheckedChange = { darkTheme = !darkTheme }) + Text( + "Dark Theme", + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + val scrollable = rememberScrollState() + Column( + modifier = Modifier + .verticalScroll(scrollable), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ComponentPreview("Colors") { + 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) + } + } + 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) + } + } + 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) + } + 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) + } + } + } + } + ComponentPreview("Text & Fonts") { + Column { + Text("displayLarge", style = MaterialTheme.typography.displayLarge) + Text("displayMedium", style = MaterialTheme.typography.displayMedium) + Text("displaySmall", style = MaterialTheme.typography.displaySmall) + + Text("headlineLarge", style = MaterialTheme.typography.headlineLarge) + Text("headlineMedium", style = MaterialTheme.typography.headlineMedium) + Text("headlineSmall", style = MaterialTheme.typography.headlineSmall) - CompositionLocalProvider(LocalTheme provides theme) { - LocaleProvider { - MaterialTheme( - colors = colors, - typography = Typography, - content = content - ) + Text("titleLarge", style = MaterialTheme.typography.titleLarge) + Text("titleMedium", style = MaterialTheme.typography.titleMedium) + Text("titleSmall", style = MaterialTheme.typography.titleSmall) + + Text("bodyLarge", style = MaterialTheme.typography.bodyLarge) + Text("bodyMedium", style = MaterialTheme.typography.bodyMedium) + Text("bodySmall", style = MaterialTheme.typography.bodySmall) + + Text("labelLarge", style = MaterialTheme.typography.labelLarge) + Text("labelMedium", style = MaterialTheme.typography.labelMedium) + Text("labelSmall", style = MaterialTheme.typography.labelSmall) + } + } + ComponentPreview("Buttons") { + Button(onClick = {}) { + Text("Filled") + } + Button(onClick = {}, enabled = false) { + Text("Disabled") + } + OutlinedButton(onClick = {}) { + Text("Outlined") + } + TextButton(onClick = {}) { + Text("Text") + } + } + ComponentPreview("Icon Buttons") { + IconButton(onClick = {}) { + Icon(Icons.Default.Map, contentDescription = "Icon Button") + } + } + ComponentPreview("Chip") { + AssistChip(onClick = {}, label = { + Text("Assist Chip") + }) + FilterChip(selected = false, onClick = {}, label = { + Text("Filter not Selected") + }) + FilterChip(selected = true, onClick = {}, label = { + Text("Filter Selected") + }) + } + ComponentPreview("Progress Indicator") { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)){ + CircularProgressIndicator() + LinearProgressIndicator() + } + } + ComponentPreview("Radio Button") { + var state by remember { mutableStateOf(true) } + RadioButton(!state, onClick = { state = false }) + RadioButton(state, onClick = { state = true }) + + } + ComponentPreview("Checkbox") { + var state by remember { mutableStateOf(true) } + Checkbox(state, onCheckedChange = { state = it }) + Checkbox(!state, onCheckedChange = { state = !it }) + Checkbox(state, onCheckedChange = {}, enabled = false) + TriStateCheckbox(ToggleableState.Indeterminate, onClick = {}) + } + ComponentPreview("Switch") { + var state by remember { mutableStateOf(true) } + Switch(state, onCheckedChange = { state = it }) + Switch(!state, enabled = false, onCheckedChange = { state = it }) + } + ComponentPreview("Slider") { + Column{ + var state by remember { mutableStateOf(0.5f) } + Slider(state, onValueChange = { state = it }) + var rangeState by remember { mutableStateOf(0.25f..0.75f) } + RangeSlider(rangeState, onValueChange = { rangeState = it }) + } + + } + ComponentPreview("Badge") { + IconButton(onClick = {}) { + BadgedBox(badge = { Badge() }) { + Icon(Icons.Default.Map, contentDescription = "Icon with Badge") + } + } + } + ComponentPreview("Number Field") { + var number by remember { mutableStateOf("123") } + TextField(number, onValueChange = { + if(it.all { char -> char.isDigit() }) { + number = it + } + }, label = { Text("Number Field") }) + + } + ComponentPreview("Text Field") { + Row { + var text by remember { mutableStateOf("Text Field") } + TextField(text, onValueChange = { text = it }) + } + var text by remember { mutableStateOf("Outlined Text Field") } + OutlinedTextField(text, onValueChange = { text = it}) + } + ComponentPreview("Dropdown Menu") { + var show by remember { mutableStateOf(false) } + AssistChip( + onClick = { show = true }, + label = { Text("Show Menu") } + ) + DropdownMenu( + expanded = show, + onDismissRequest = { + show = false + }, + ) { + DropdownMenuItem(onClick = { show = false }, text = { + Text("Menu Item 1", modifier = Modifier.padding(8.dp)) + }) + DropdownMenuItem(onClick = { show = false }, text = { + Text("Menu Item 2", modifier = Modifier.padding(8.dp)) + }) + DropdownMenuItem(onClick = { show = false }, text = { + Text("Menu Item 3", modifier = Modifier.padding(8.dp)) + }) + } + + + } + + ComponentPreview("Scrollable View") { + + } + + ComponentPreview("Tabs") { + + } + } + } } } } } -fun String.toColorInt(): Int { - if (this[0] == '#') { - var color = substring(1).toLong(16) - if (length == 7) { - color = color or 0x00000000ff000000L - } else if (length != 9) { - throw IllegalArgumentException("Unknown color") +@Composable +private fun ComponentPreview(title: String, content: @Composable () -> Unit) { + Column { + Text(title, style = MaterialTheme.typography.titleLarge) + HorizontalDivider() + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(vertical = 8.dp)) { + content() } - return color.toInt() + HorizontalDivider() } - throw IllegalArgumentException("Unknown color") } \ No newline at end of file diff --git a/app/src/processing/app/ui/theme/Typography.kt b/app/src/processing/app/ui/theme/Typography.kt index 5d87c490e6..6650ac7167 100644 --- a/app/src/processing/app/ui/theme/Typography.kt +++ b/app/src/processing/app/ui/theme/Typography.kt @@ -1,6 +1,5 @@ package processing.app.ui.theme -import androidx.compose.material.MaterialTheme.typography import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -21,18 +20,108 @@ val processingFont = FontFamily( style = FontStyle.Normal ) ) +val spaceGroteskFont = FontFamily( + Font( + resource = "SpaceGrotesk-Bold.ttf", + weight = FontWeight.Bold, + ), + Font( + resource = "SpaceGrotesk-Regular.ttf", + weight = FontWeight.Normal, + ), + Font( + resource = "SpaceGrotesk-Medium.ttf", + weight = FontWeight.Medium, + ), + Font( + resource = "SpaceGrotesk-SemiBold.ttf", + weight = FontWeight.SemiBold, + ), + Font( + resource = "SpaceGrotesk-Light.ttf", + weight = FontWeight.Light, + ) +) -val Typography = Typography( +@Deprecated("Use PDE3Typography instead") +val PDE2Typography = Typography( + defaultFontFamily = spaceGroteskFont, + h1 = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 42.725.sp, + lineHeight = 48.sp + ), + h2 = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 34.18.sp, + lineHeight = 40.sp + ), + h3 = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 27.344.sp, + lineHeight = 32.sp + ), + h4 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 21.875.sp, + lineHeight = 28.sp + ), + h5 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 17.5.sp, + lineHeight = 22.sp + ), + h6 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 18.sp + ), body1 = TextStyle( - fontFamily = processingFont, fontWeight = FontWeight.Normal, - fontSize = 13.sp, + fontSize = 14.sp, + lineHeight = 18.sp + ), + body2 = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.8.sp, lineHeight = 16.sp ), subtitle1 = TextStyle( - fontFamily = processingFont, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 20.sp + ), + subtitle2 = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 13.824.sp, + lineHeight = 16.sp, + ), + caption = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 11.2.sp, + lineHeight = 14.sp + ), + overline = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 8.96.sp, + lineHeight = 10.sp ) +) +val base = androidx.compose.material3.Typography() +val PDETypography = androidx.compose.material3.Typography( + displayLarge = base.displayLarge.copy(fontFamily = spaceGroteskFont), + displayMedium = base.displayMedium.copy(fontFamily = spaceGroteskFont), + displaySmall = base.displaySmall.copy(fontFamily = spaceGroteskFont), + headlineLarge = base.headlineLarge.copy(fontFamily = spaceGroteskFont), + headlineMedium = base.headlineMedium.copy(fontFamily = spaceGroteskFont), + headlineSmall = base.headlineSmall.copy(fontFamily = spaceGroteskFont), + titleLarge = base.titleLarge.copy(fontFamily = spaceGroteskFont), + titleMedium = base.titleMedium.copy(fontFamily = spaceGroteskFont), + titleSmall = base.titleSmall.copy(fontFamily = spaceGroteskFont), + bodyLarge = base.bodyLarge.copy(fontFamily = spaceGroteskFont), + bodyMedium = base.bodyMedium.copy(fontFamily = spaceGroteskFont), + bodySmall = base.bodySmall.copy(fontFamily = spaceGroteskFont), + labelLarge = base.labelLarge.copy(fontFamily = spaceGroteskFont), + labelMedium = base.labelMedium.copy(fontFamily = spaceGroteskFont), + labelSmall = base.labelSmall.copy(fontFamily = spaceGroteskFont), ) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8e7ad44a7a..6c8c5262cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ plugins { kotlin("jvm") version libs.versions.kotlin apply false - alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.jetbrainsCompose) apply false diff --git a/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf b/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0408641c61d0f3ce53d24348e9db72dcc1261179 GIT binary patch literal 86520 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 literal 0 HcmV?d00001 diff --git a/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf b/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..981bcf5b2cb0a1ca43dcee7577015c0bfbdf91fb GIT binary patch literal 86592 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 Sp;;r@Vp7<@IS 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 literal 0 HcmV?d00001 diff --git a/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf b/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7e02e51e48209e508954e9fdc08a1036d752702 GIT binary patch literal 86576 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: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 WBn3EMsDPe7sg0 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 Date: Wed, 22 Oct 2025 07:26:40 +0200 Subject: [PATCH 10/18] Switch from ProcessingTheme to PDETheme in window UI Replaces the use of ProcessingTheme with PDETheme in the PDEWindowContent composable --- app/src/processing/app/ui/theme/Window.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index 91d245089e..7b38e774cb 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -84,7 +84,7 @@ private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent } CompositionLocalProvider(LocalWindow provides window) { - ProcessingTheme { + PDETheme { val locale = LocalLocale.current window.title = locale[titleKey] LaunchedEffect(locale) { From 1c26bab4c2020b5d8da6126bfba99255ace35ed4 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 23 Oct 2025 08:26:22 +0200 Subject: [PATCH 11/18] Refactor preferences to Jetpack Compose UI Replaces the legacy PreferencesFrame with a new Jetpack Compose-based preferences UI. Adds reactive preferences management using a custom ReactiveProperties class, and introduces modular preference groups (General, Interface, Other) with composable controls. Updates Base.java to launch the new preferences window, and refactors theme and window code for Compose integration. --- app/src/processing/app/Base.java | 10 +- app/src/processing/app/Preferences.java | 8 + app/src/processing/app/Preferences.kt | 163 +++++++-- app/src/processing/app/ui/Preferences.kt | 325 ++++++++++++++++++ app/src/processing/app/ui/WelcomeToBeta.kt | 1 - .../processing/app/ui/preferences/General.kt | 121 +++++++ .../app/ui/preferences/Interface.kt | 168 +++++++++ .../processing/app/ui/preferences/Other.kt | 73 ++++ app/src/processing/app/ui/theme/Theme.kt | 2 +- app/src/processing/app/ui/theme/Window.kt | 122 +++++-- 10 files changed, 937 insertions(+), 56 deletions(-) create mode 100644 app/src/processing/app/ui/Preferences.kt create mode 100644 app/src/processing/app/ui/preferences/General.kt create mode 100644 app/src/processing/app/ui/preferences/Interface.kt create 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 2551a54d64..e3eae12fb8 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -40,6 +40,7 @@ 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.data.StringList; @@ -2190,10 +2191,11 @@ static private Mode findSketchMode(File folder, List modeList) { * Show the Preferences window. */ public void handlePrefs() { - if (preferencesFrame == null) { - preferencesFrame = new PreferencesFrame(this); - } - preferencesFrame.showFrame(); +// if (preferencesFrame == null) { +// preferencesFrame = new PreferencesFrame(this); +// } +// preferencesFrame.showFrame(); + PreferencesKt.show(); } diff --git a/app/src/processing/app/Preferences.java b/app/src/processing/app/Preferences.java index 640c77eade..8fcf7bb056 100644 --- a/app/src/processing/app/Preferences.java +++ b/app/src/processing/app/Preferences.java @@ -136,6 +136,14 @@ static public void skipInit() { initialized = true; } + /** + * Check whether Preferences.init() has been called. If not, we are probably not running the full application. + * @return true if Preferences has been initialized + */ + static public boolean isInitialized() { + return initialized; + } + static void handleProxy(String protocol, String hostProp, String portProp) { String proxyHost = get("proxy." + protocol + ".host"); 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/src/processing/app/ui/Preferences.kt b/app/src/processing/app/ui/Preferences.kt new file mode 100644 index 0000000000..7fd9f56350 --- /dev/null +++ b/app/src/processing/app/ui/Preferences.kt @@ -0,0 +1,325 @@ +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/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt index 72c3050006..531c28f7ef 100644 --- a/app/src/processing/app/ui/WelcomeToBeta.kt +++ b/app/src/processing/app/ui/WelcomeToBeta.kt @@ -42,7 +42,6 @@ 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.ProcessingTheme import java.awt.Cursor import java.awt.Dimension import java.awt.event.KeyAdapter diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt new file mode 100644 index 0000000000..5f56187f46 --- /dev/null +++ b/app/src/processing/app/ui/preferences/General.kt @@ -0,0 +1,121 @@ +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 new file mode 100644 index 0000000000..fc384fbc59 --- /dev/null +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -0,0 +1,168 @@ +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.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 + var showOptions by remember { mutableStateOf(false) } + val languages = if(Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") + TextField( + value = locale.locale.displayName, + readOnly = true, + onValueChange = { }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "Select Font Family", + modifier = Modifier + .clickable{ + showOptions = true + } + ) + } + ) + DropdownMenu( + expanded = showOptions, + onDismissRequest = { + showOptions = false + }, + ) { + languages.forEach { family -> + DropdownMenuItem( + text = { Text(family.value) }, + onClick = { + locale.set(Locale(family.key)) + showOptions = false + } + ) + } + } + } + )) + + 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, + ) + } + } + )) + } + } +} \ 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 new file mode 100644 index 0000000000..f5f65ea9c8 --- /dev/null +++ b/app/src/processing/app/ui/preferences/Other.kt @@ -0,0 +1,73 @@ +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 diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index 7cc70455f0..9e41227ed1 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -93,7 +93,7 @@ fun PDETheme( colorScheme = if(darkTheme) PDEDarkColor else PDELightColor, typography = PDETypography ){ - Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background).fillMaxSize()) { + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { CompositionLocalProvider( LocalContentColor provides MaterialTheme.colorScheme.onBackground, LocalDensity provides Density(1.25f, 1.25f), diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index 7b38e774cb..bf998d5742 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -1,6 +1,8 @@ package processing.app.ui.theme +import androidx.compose.foundation.background 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 @@ -9,16 +11,22 @@ 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 val LocalWindow = compositionLocalOf { error("No Window Set") } @@ -37,32 +45,46 @@ val LocalWindow = compositionLocalOf { error("No Window Set") } * ``` * * @param titleKey The key for the window title, which will be localized. + * @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 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. */ -class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable BoxScope.() -> Unit): JFrame(){ +// TODO: Add support for onClose callback +class PDESwingWindow( + titleKey: String = "", + size: Dimension? = null, + minSize: Dimension? = null, + maxSize: Dimension? = null, + fullWindowContent: Boolean = false, + onClose: () -> Unit = {}, + content: @Composable () -> Unit +){ init{ - val window = this - defaultCloseOperation = DISPOSE_ON_CLOSE - ComposePanel().apply { + ComposeWindow().apply { + val window = this + defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE + size?.let { + window.size = it + } + minSize?.let { + window.minimumSize = it + } + maxSize?.let { + window.maximumSize = it + } + setLocationRelativeTo(null) setContent { PDEWindowContent(window, titleKey, fullWindowContent, content) } - window.add(this) - } - background = java.awt.Color.white - setLocationRelativeTo(null) - addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - if (e.keyCode != KeyEvent.VK_ESCAPE) return - - window.dispose() - onClose() + window.addWindowStateListener { + if(it.newState == JFrame.DISPOSE_ON_CLOSE){ + onClose() + } } - }) - isResizable = false - isVisible = true - requestFocus() + isVisible = true + } } } @@ -76,7 +98,12 @@ class PDESwingWindow(titleKey: String = "", fullWindowContent: Boolean = false, * @param content The composable content to be displayed in the window. */ @Composable -private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent: Boolean = false, content: @Composable BoxScope.() -> Unit){ +private fun PDEWindowContent( + window: ComposeWindow, + titleKey: String, + fullWindowContent: Boolean = false, + content: @Composable () -> Unit +){ val mac = SystemInfo.isMacOS && SystemInfo.isMacFullWindowContentSupported remember { window.rootPane.putClientProperty("apple.awt.fullWindowContent", mac && fullWindowContent) @@ -84,15 +111,10 @@ private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent } CompositionLocalProvider(LocalWindow provides window) { - PDETheme { + PDETheme{ val locale = LocalLocale.current window.title = locale[titleKey] - LaunchedEffect(locale) { - window.pack() - window.setLocationRelativeTo(null) - } - - Box(modifier = Modifier.padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp),content = content) + content() } } } @@ -123,6 +145,10 @@ private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent * ``` * * @param titleKey The key for the window title, which will be localized. + * @param size The desired size of the window. Defaults to unspecified size which means the window will be + * 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 onClose A lambda function to be called when the window is requested to close. @@ -132,12 +158,52 @@ private fun PDEWindowContent(window: JFrame, titleKey: String, fullWindowContent * */ @Composable -fun PDEComposeWindow(titleKey: String, fullWindowContent: Boolean = false, onClose: () -> Unit = {}, content: @Composable BoxScope.() -> Unit){ +fun PDEComposeWindow( + titleKey: String, + size: DpSize = DpSize.Unspecified, + minSize: DpSize = DpSize.Unspecified, + maxSize: DpSize = DpSize.Unspecified, + fullWindowContent: Boolean = false, + onClose: () -> Unit = {}, + content: @Composable () -> Unit +){ val windowState = rememberWindowState( - size = DpSize.Unspecified, + size = size, position = WindowPosition(Alignment.Center) ) Window(onCloseRequest = onClose, state = windowState, title = "") { + remember { + window.minimumSize = minSize.toDimension() + window.maximumSize = maxSize.toDimension() + } PDEWindowContent(window, titleKey, fullWindowContent, content) } +} + +fun DpSize.toDimension(): Dimension? { + if(this == DpSize.Unspecified) { return null } + + return Dimension( + this.width.value.toInt(), + this.height.value.toInt() + ) +} + +fun main(){ + application { + PDEComposeWindow( + onClose = ::exitApplication, + titleKey = "window.title", + size = DpSize(800.dp, 600.dp), + ){ + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Text("Hello, World!") + } + } + } } \ No newline at end of file From 8fe058527743a09f9845ccc37925754f590d3dbb Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 23 Oct 2025 09:00:59 +0200 Subject: [PATCH 12/18] Remove obsolete TODO for onClose callback --- app/src/processing/app/ui/theme/Window.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt index bf998d5742..98a4e00807 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -51,7 +51,6 @@ val LocalWindow = compositionLocalOf { error("No Window Set") } * @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. */ -// TODO: Add support for onClose callback class PDESwingWindow( titleKey: String = "", size: Dimension? = null, From 5a6f7fa5bd34781e27739b78e17ec039446193f6 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Fri, 24 Oct 2025 11:32:23 +0200 Subject: [PATCH 13/18] Clean up handlePrefs method by removing comments Removed commented-out code for preferences frame initialization. --- app/src/processing/app/Base.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index e3eae12fb8..a1604ff0a0 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -2191,10 +2191,6 @@ 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(); } From e9b381ba9b5cd882dba60601986cfc1c776af709 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Tue, 28 Oct 2025 05:12:58 +0100 Subject: [PATCH 14/18] Composable Preferences rewrite (#1277) * 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 * 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. * Small bugfix for removed function * Add compose ui test to the deps --- app/test/processing/app/PreferencesKtTest.kt | 61 ++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/test/processing/app/PreferencesKtTest.kt 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 e1fcdb1f5b59c81f50515893e30dafc401aa868a Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 26 Nov 2025 22:42:38 +0100 Subject: [PATCH 15/18] Welcome screen implementation (#1307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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. * Small bugfix for removed function * Add compose ui test to the deps * 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. * 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. * Initial layout * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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 * 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. * Removing the Preferences work to keep the PR clean * 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. * Tweak welcome actions naming and order - Rename `Empty Sketch` to `New Sketch` - Rename `Sketchbook` to `My Sketches` - Move `Open Examples` below `My Sketches` * Rather than setting the decorations app wide, just modify the editor screen --------- Co-authored-by: Raphaël de Courville --- .../processing/app/ui/WebFrame.java | 0 .../processing/app/ui/Welcome.java | 0 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 | 44 +- app/src/processing/app/Messages.kt | 45 ++ app/src/processing/app/api/Contributions.kt | 210 +++--- .../app/platform/LinuxPlatform.java | 9 +- app/src/processing/app/ui/Editor.java | 62 +- app/src/processing/app/ui/PDEWelcome.kt | 610 ++++++++++++++++++ app/src/processing/app/ui/Preferences.kt | 325 ---------- app/src/processing/app/ui/Start.kt | 2 + .../processing/app/ui/preferences/General.kt | 121 ---- .../app/ui/preferences/Interface.kt | 168 ----- .../processing/app/ui/preferences/Other.kt | 73 --- app/src/processing/app/ui/theme/Colors.kt | 46 +- app/src/processing/app/ui/theme/Theme.kt | 175 +++-- app/src/processing/app/ui/theme/Window.kt | 56 +- app/src/processing/app/ui/theme/m3/Color.kt | 248 +++++++ app/src/processing/app/ui/theme/m3/Theme.kt | 301 +++++++++ build/shared/lib/languages/PDE.properties | 17 + build/shared/lib/languages/PDE_nl.properties | 16 + java/src/processing/mode/java/JavaEditor.java | 8 +- 24 files changed, 1545 insertions(+), 1007 deletions(-) rename app/{src => ant}/processing/app/ui/WebFrame.java (100%) rename app/{src => ant}/processing/app/ui/Welcome.java (100%) 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 create mode 100644 app/src/processing/app/ui/PDEWelcome.kt 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 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/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/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 a1604ff0a0..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 @@ -375,13 +376,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(base); } } @@ -609,7 +604,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; @@ -1885,7 +1880,7 @@ public void handleRestart() { // } - public void thinkDifferentExamples() { + public void showExamplesFrame() { nextMode.showExamplesFrame(); } @@ -2191,7 +2186,10 @@ static private Mode findSketchMode(File folder, List modeList) { * Show the Preferences window. */ public void handlePrefs() { - 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 cae54e6e97..fa9bc54a63 100644 --- a/app/src/processing/app/Messages.kt +++ b/app/src/processing/app/Messages.kt @@ -18,13 +18,27 @@ */ package processing.app +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +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.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 { @@ -270,6 +284,37 @@ class Messages { } } } +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 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/platform/LinuxPlatform.java b/app/src/processing/app/platform/LinuxPlatform.java index 3426144cae..647412415e 100644 --- a/app/src/processing/app/platform/LinuxPlatform.java +++ b/app/src/processing/app/platform/LinuxPlatform.java @@ -22,16 +22,13 @@ package processing.app.platform; -import java.io.File; -import java.awt.Desktop; -import java.awt.Toolkit; - import processing.app.Base; import processing.app.Messages; import processing.app.Preferences; import processing.core.PApplet; -import javax.swing.*; +import java.awt.*; +import java.io.File; public class LinuxPlatform extends DefaultPlatform { @@ -40,8 +37,6 @@ public class LinuxPlatform extends DefaultPlatform { public void initBase(Base base) { super.initBase(base); - - JFrame.setDefaultLookAndFeelDecorated(true); System.setProperty("flatlaf.menuBarEmbedded", "true"); // Set X11 WM_CLASS property which is used as the application diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index e4b4f15879..0437240b37 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -23,38 +23,42 @@ package processing.app.ui; -import java.awt.*; -import java.awt.datatransfer.*; -import java.awt.event.*; -import java.awt.print.*; -import java.io.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Stack; -import java.util.Timer; -import java.util.TimerTask; -import java.util.stream.Collectors; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.*; -import javax.swing.plaf.basic.*; -import javax.swing.text.*; -import javax.swing.text.html.*; -import javax.swing.undo.*; - import com.formdev.flatlaf.util.SystemInfo; import processing.app.*; -import processing.utils.SketchException; +import processing.app.Formatter; import processing.app.contrib.ContributionManager; import processing.app.laf.PdeMenuItemUI; import processing.app.syntax.*; -import processing.core.*; +import processing.core.PApplet; +import processing.utils.SketchException; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.*; +import javax.swing.plaf.basic.BasicSplitPaneDivider; +import javax.swing.plaf.basic.BasicSplitPaneUI; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; +import javax.swing.text.View; +import javax.swing.text.ViewFactory; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.CompoundEdit; +import javax.swing.undo.UndoManager; +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.event.*; +import java.awt.print.PageFormat; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.List; +import java.util.Timer; +import java.util.stream.Collectors; /** @@ -207,6 +211,10 @@ public void windowDeactivated(WindowEvent e) { spacer.setAlignmentX(Component.LEFT_ALIGNMENT); box.add(spacer); } + if (Platform.isLinux()) { + setUndecorated(true); + getRootPane().setWindowDecorationStyle(JRootPane.FRAME); + } rebuildModePopup(); toolbar = createToolbar(); diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt new file mode 100644 index 0000000000..4c8e79183e --- /dev/null +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -0,0 +1,610 @@ +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.slideIn +import androidx.compose.animation.slideOut +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.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.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.ImageBitmap +import androidx.compose.ui.graphics.decodeToImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +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.* +import processing.app.api.Contributions.ExamplesList.Companion.listAllExamples +import processing.app.api.Sketch.Companion.Sketch +import processing.app.ui.theme.* +import java.io.File +import kotlin.io.path.Path +import kotlin.io.path.exists + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PDEWelcome(base: Base? = null) { + Row( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLowest), + ){ + val shape = RoundedCornerShape(12.dp) + val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) + val xsModifier = Modifier + .defaultMinSize(minHeight = 1.dp) + .height(32.dp) + val textColor = if(isSystemInDarkTheme()) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSecondaryContainer + val locale = LocalLocale.current + + /** + * Left main column + */ + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxHeight() + .weight(0.8f) + .padding( + top = 48.dp, + start = 56.dp, + end = 64.dp, + bottom = 56.dp + ) + ) { + /** + * Title row + */ + Row ( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ){ + Image( + painter = painterResource("logo.svg"), + modifier = Modifier + .size(50.dp), + contentDescription = locale["welcome.processing.logo"] + ) + Text( + text = locale["welcome.processing.title"], + 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, + shape = shape + ){ + 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) + } + } + } + /** + * 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.showSketchbookFrame() + } ?: noBaseWarning() + }, + colors = colors, + modifier = medModifier, + shape = shape + ) { + Icon(Icons.Outlined.FolderOpen, contentDescription = "") + Spacer(Modifier.width(12.dp)) + Text(locale["welcome.actions.sketchbook"], modifier = Modifier.align(Alignment.CenterVertically)) + } + 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"]) + } + } + } + /** + * Resources and community card + */ + Card( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ){ + Row( + horizontalArrangement = Arrangement.spacedBy(48.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( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = locale["welcome.resources.title"], + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + modifier = Modifier.padding(start = 8.dp) + ) + TextButton( + onClick = { + Platform.openURL("https://processing.org/tutorials/gettingstarted") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Outlined.PinDrop, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text( + text = locale["welcome.resources.get_started"], + ) + } + TextButton( + onClick = { + Platform.openURL("https://processing.org/tutorials") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Outlined.School, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text( + text = locale["welcome.resources.tutorials"], + ) + } + TextButton( + onClick = { + Platform.openURL("https://processing.org/reference") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon(Icons.Outlined.Book, contentDescription = "", modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(4.dp)) + Text( + text = locale["welcome.resources.documentation"], + ) + } + } + 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(48.dp), + modifier = Modifier + .fillMaxWidth() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + 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( + text = locale["welcome.community.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( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + TextButton( + onClick = { + Platform.openURL("https://github.com/processing/processing4") + }, + 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://www.instagram.com/processing_core/") + }, + contentPadding = xsPadding, + modifier = xsModifier, + colors = colors + ) { + Icon( + painterResource("icons/Instagram.svg"), + contentDescription = "", + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.width(4.dp)) + Text("Instagram") + } + } + } + } + } + } + } + /** + * 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 + .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 + ) + } + } + } + /** + * Examples list + */ + val scrollMargin = 35.dp + Column( + modifier = Modifier + .width(350.dp + scrollMargin) + ) { + val examples = remember { mutableStateListOf( + *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 { + 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 + } + examples.clear() + examples.addAll(sketches.shuffled().take(20)) + } + val state = rememberLazyListState( + initialFirstVisibleItemScrollOffset = 150 + ) + Box( + modifier = Modifier + .padding(end = 4.dp) + ) { + LazyColumn( + state = state, + contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp, end = 20.dp, start = scrollMargin), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(examples) { example -> + example.card{ + base?.let { + base.handleOpen("${example.path}/${example.name}.pde") + } ?: noBaseWarning() + } + } + } + VerticalScrollbar( + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterEnd), + adapter = rememberScrollbarAdapter(state) + ) + } + } + } +} + +@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( + "No Base", + "No Base instance provided, this ui is likely being previewed." + ) +} + +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(), + unique = WelcomeScreen::class, + fullWindowContent = true + ) { + PDEWelcome(base) + } +} + +@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 { + PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { + PDETheme(darkTheme = true) { + PDEWelcome() + } + } + PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { + PDETheme(darkTheme = false) { + PDEWelcome() + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/Preferences.kt b/app/src/processing/app/ui/Preferences.kt deleted file mode 100644 index 7fd9f56350..0000000000 --- a/app/src/processing/app/ui/Preferences.kt +++ /dev/null @@ -1,325 +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/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/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 fc384fbc59..0000000000 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ /dev/null @@ -1,168 +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.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 - var showOptions by remember { mutableStateOf(false) } - val languages = if(Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English") - TextField( - value = locale.locale.displayName, - readOnly = true, - onValueChange = { }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "Select Font Family", - modifier = Modifier - .clickable{ - showOptions = true - } - ) - } - ) - DropdownMenu( - expanded = showOptions, - onDismissRequest = { - showOptions = false - }, - ) { - languages.forEach { family -> - DropdownMenuItem( - text = { Text(family.value) }, - onClick = { - locale.set(Locale(family.key)) - showOptions = false - } - ) - } - } - } - )) - - 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, - ) - } - } - )) - } - } -} \ 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 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..c59c5025cd 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -1,61 +1,24 @@ package processing.app.ui.theme -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.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -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.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 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,13 +53,21 @@ 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, - LocalDensity provides Density(1.25f, 1.25f), + LocalScrollbarStyle provides ScrollbarStyle( + minimalHeight = 16.dp, + thickness = 8.dp, + shape = MaterialTheme.shapes.extraSmall, + hoverDurationMillis = 300, + unhoverColor = MaterialTheme.colorScheme.outlineVariant, + hoverColor = MaterialTheme.colorScheme.outlineVariant + ), + LocalContentColor provides MaterialTheme.colorScheme.onSurface, +// LocalDensity provides Density(1.25f, 1.25f), content = content ) } @@ -137,66 +108,64 @@ 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) + 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 +306,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/Window.kt b/app/src/processing/app/ui/theme/Window.kt index 98a4e00807..f725a999b5 100644 --- a/app/src/processing/app/ui/theme/Window.kt +++ b/app/src/processing/app/ui/theme/Window.kt @@ -5,28 +5,26 @@ 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 processing.app.ui.Toolkit import java.awt.Dimension -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent import javax.swing.JFrame -import javax.swing.UIManager +import javax.swing.JRootPane +import kotlin.reflect.KClass val LocalWindow = compositionLocalOf { error("No Window Set") } @@ -48,6 +46,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 +56,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 +76,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 +94,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 +110,7 @@ class PDESwingWindow( private fun PDEWindowContent( window: ComposeWindow, titleKey: String, + unique: KClass<*>? = null, fullWindowContent: Boolean = false, content: @Composable () -> Unit ){ @@ -107,6 +118,21 @@ 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() + window.dispose() + return + } + + DisposableEffect(unique){ + unique?.let { + windows[it] = window + } + onDispose { + windows.remove(unique) + } } CompositionLocalProvider(LocalWindow provides window) { @@ -148,13 +174,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 +185,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 +199,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 + ) } } 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 diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 19a5c9f866..736ac2e6ba 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -640,6 +640,23 @@ 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 = New Sketch +welcome.actions.examples = Open Examples +welcome.actions.sketchbook = My Sketches +welcome.actions.show_startup = Show this window at startup +welcome.resources.title = Resources +welcome.resources.get_started = Get Started +welcome.resources.tutorials = Tutorials +welcome.resources.documentation = Reference +welcome.community.title = Join our community +welcome.community.forum = Forum +welcome.sketch.open = Open + # --------------------------------------- # 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... diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 3fab2c8b17..7ce9e45be7 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(base); }); menu.add(item); From 4b39a236cbc03a860c2c88da36009ee3376afbd8 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 8 Dec 2025 06:10:02 +0100 Subject: [PATCH 16/18] Replace ProcessingTheme with PDETheme in WelcomeSurvey --- app/src/processing/app/ui/WelcomeSurvey.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/processing/app/ui/WelcomeSurvey.kt b/app/src/processing/app/ui/WelcomeSurvey.kt index ecc771c278..825f82827c 100644 --- a/app/src/processing/app/ui/WelcomeSurvey.kt +++ b/app/src/processing/app/ui/WelcomeSurvey.kt @@ -18,14 +18,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import processing.app.Platform import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.ProcessingTheme +import processing.app.ui.theme.PDETheme import javax.swing.JComponent fun addSurveyToWelcomeScreen(): JComponent { return ComposePanel().apply { setContent { - ProcessingTheme { + PDETheme { val locale = LocalLocale.current Box { Row( From 243789b3f5ee1b89c55954fea8388bdd6c151eab Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 8 Dec 2025 06:14:14 +0100 Subject: [PATCH 17/18] Add Material Theme Builder file headers Added autogenerated file headers to Color.kt and Theme.kt indicating they were generated by the Material Theme Builder tool and should not be edited directly. Also reordered imports in Theme.kt for consistency. --- app/src/processing/app/ui/theme/m3/Color.kt | 4 ++++ app/src/processing/app/ui/theme/m3/Theme.kt | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/processing/app/ui/theme/m3/Color.kt b/app/src/processing/app/ui/theme/m3/Color.kt index b2047ce7e6..e1272750b3 100644 --- a/app/src/processing/app/ui/theme/m3/Color.kt +++ b/app/src/processing/app/ui/theme/m3/Color.kt @@ -1,3 +1,7 @@ +/** + * This file was generated by the Material Theme Builder tool. + * Do not edit this file directly. + */ import androidx.compose.ui.graphics.Color val primaryLight = Color(0xFF525A92) diff --git a/app/src/processing/app/ui/theme/m3/Theme.kt b/app/src/processing/app/ui/theme/m3/Theme.kt index d1b2f403a8..86ece8f9e0 100644 --- a/app/src/processing/app/ui/theme/m3/Theme.kt +++ b/app/src/processing/app/ui/theme/m3/Theme.kt @@ -1,7 +1,10 @@ +/** + * This file was generated by the Material Theme Builder tool. + * Do not edit this file directly. + */ - -import androidx.compose.material3.lightColorScheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color From 8883c023c94458f0b4cccb6adfe29cc918c6fda7 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 8 Dec 2025 06:22:16 +0100 Subject: [PATCH 18/18] Fix preferences file override and update test property Corrects the logic for selecting the preferences file in PreferencesProvider to use the override if present. Updates the test to set the correct system property for the settings folder. --- app/src/processing/app/Preferences.kt | 2 +- app/test/processing/app/LocaleKtTest.kt | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index 56e0eedfd1..490110f8ac 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -80,7 +80,7 @@ fun PreferencesProvider(content: @Composable () -> Unit) { val preferencesDebounceOverride: Long? = System.getProperty("processing.app.preferences.debounce")?.toLongOrNull() val settingsFolder = Settings.getFolder() - val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME) + val preferencesFile = preferencesFileOverride ?: settingsFolder.resolve(PREFERENCES_FILE_NAME) if (!preferencesFile.exists()) { preferencesFile.mkdirs() diff --git a/app/test/processing/app/LocaleKtTest.kt b/app/test/processing/app/LocaleKtTest.kt index f8ed32164a..662fd1120a 100644 --- a/app/test/processing/app/LocaleKtTest.kt +++ b/app/test/processing/app/LocaleKtTest.kt @@ -4,11 +4,7 @@ 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.ExperimentalTestApi -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.runComposeUiTest +import androidx.compose.ui.test.* import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.LocaleProvider import kotlin.io.path.createTempDirectory @@ -20,7 +16,7 @@ class LocaleKtTest { fun testLocale() = runComposeUiTest { val tempPreferencesDir = createTempDirectory("preferences") - System.setProperty("processing.app.preferences.folder", tempPreferencesDir.toFile().absolutePath) + System.setProperty("processing.settings.folder", tempPreferencesDir.toFile().absolutePath) setContent { LocaleProvider {