Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b1910de
Welcome Screen: Initial Frame
Stefterv Feb 7, 2025
bd3a77e
Welcome Screen: Window Abstraction
Stefterv Feb 7, 2025
5c020dd
Welcome Screen: Initial Layout
Stefterv Feb 7, 2025
8ed2d1b
Welcome Screen: WIP
Stefterv Feb 7, 2025
2f12d13
Saveable/Reactive Preferences
Stefterv Feb 7, 2025
b0c61a4
Welcome Screen: Load & Display examples
Stefterv Feb 8, 2025
3d81b02
Welcome Screen: Language selection
Stefterv Feb 8, 2025
cb184fb
Welcome Screen: Layout fixes
Stefterv Feb 9, 2025
3df4da9
Welcome Screen: Bugfixes
Stefterv Feb 9, 2025
8d8bd01
Welcome Screen: Close Functionality
Stefterv Feb 9, 2025
54944af
Welcome Screen: JDK Module
Stefterv Feb 9, 2025
0e56f89
Merge branch 'main-gradle' into gradle-welcome-screen
Stefterv Feb 9, 2025
babf54e
Merge branch 'gradle-loggin' into gradle-welcome-screen
Stefterv Feb 10, 2025
7452d65
Merge branch 'main' into gradle-welcome-screen
Stefterv Mar 7, 2025
6974811
Merge branch 'test-schema' into gradle-welcome-screen
Stefterv Mar 11, 2025
0df4dc0
Merge branch 'main' into gradle-welcome-screen
Stefterv Mar 16, 2025
b0e7f96
Added initial tests
Stefterv Mar 17, 2025
d17ca0c
Removed PlatformStart and create new file if doesn't exist
Stefterv Mar 17, 2025
f7f22c5
Create Directory as well
Stefterv Mar 17, 2025
3d6a6ce
Merge branch 'processing:main' into gradle-welcome-screen
Stefterv Mar 30, 2025
1a3bc46
Update bug1532.pde
Stefterv Sep 10, 2025
4702803
Merge branch 'main' into gradle-welcome-screen
Stefterv Sep 11, 2025
f36a135
Merge branch 'develop-menu' into gradle-welcome-screen
Stefterv Sep 11, 2025
4135460
Add test message triggers to Develop menu
Stefterv Sep 11, 2025
38c8001
Enhance Preferences reactivity and test coverage
Stefterv Oct 14, 2025
750ff8d
Refactor Locale class and add LocaleProvider test
Stefterv Oct 15, 2025
4546a38
Refactor Locale class and add LocaleProvider test
Stefterv Oct 15, 2025
643ec03
Make setLocale parameter nullable in Locale class
Stefterv Oct 15, 2025
06e3094
Add compose ui test to the deps
Stefterv Oct 14, 2025
d42fb2f
Update locale change method in test
Stefterv Oct 15, 2025
b6a0a86
Merge branch 'language-provider' into gradle-welcome-screen
Stefterv Oct 15, 2025
c754e5e
Refactor window creation for Compose and Swing
Stefterv Oct 15, 2025
7283c99
Enable automatic release for Maven Central publishing (#1286)
Stefterv Oct 15, 2025
c1d6313
Fix duplicate include for app:utils module (#1285)
Stefterv Oct 15, 2025
5e5f7b9
Refactor theme system and migrate to Material3
Stefterv Oct 16, 2025
0406f6a
Migrate UI components to Material3
Stefterv Oct 16, 2025
d3681f3
Remove ContributionManager and ContributionPane UI files (#1276)
Stefterv Oct 16, 2025
bf4d163
Refactor Locale class and add LocaleProvider test (#1283)
Stefterv Oct 17, 2025
4ea9640
Remove material3 theme references
Stefterv Oct 20, 2025
62702d4
Add Material3 theme support and PDETheme composable
Stefterv Oct 20, 2025
02af9b3
Add color containers to theme and update previews
Stefterv Oct 20, 2025
27efb0b
Update color schemes to Material Theme Builder output
Stefterv Oct 20, 2025
1b564d8
Revert "Update color schemes to Material Theme Builder output"
Stefterv Oct 20, 2025
a996ec3
Add RangeSlider to theme component previews
Stefterv Oct 21, 2025
213288a
Refactor theme naming and update usage in UI
Stefterv Oct 21, 2025
ad1268f
Add Material3-based Processing theme and typography
Stefterv Oct 21, 2025
408b87f
Refactor to use Material3 and update theme usage
Stefterv Oct 21, 2025
38a990f
Add Space Grotesk font files and license
Stefterv Oct 21, 2025
77c12a4
Update markdown renderer to m3 and adjust UI
Stefterv Oct 21, 2025
266dba0
Merge remote-tracking branch 'upstream/welcome-screen' into base-theme
Stefterv Oct 21, 2025
685b6d5
Merge branch 'base-theme' into gradle-welcome-screen
Stefterv Oct 21, 2025
b2f953f
Add Preferences UI with Compose and NavigationRail
Stefterv Oct 21, 2025
490c3a5
Refactor preferences UI and add General group
Stefterv Oct 21, 2025
e664b2c
Add new general preferences options
Stefterv Oct 21, 2025
2f094eb
Refactor preferences and add Other group
Stefterv Oct 22, 2025
8494109
Add Interface preferences group and UI improvements
Stefterv Oct 22, 2025
1eccd7a
Refactor preferences window to use Compose UI
Stefterv Oct 22, 2025
2412c64
Set default window size and clean up window packing
Stefterv Oct 22, 2025
6bc9721
Refactor PDESwingWindow to use ComposeWindow
Stefterv Oct 22, 2025
9ab5547
Add documentation for preferences UI components
Stefterv Oct 23, 2025
d8892b6
Add window sizing options to PDESwingWindow and PDEComposeWindow
Stefterv Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,6 +17,7 @@ plugins{

alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrainsCompose)

alias(libs.plugins.serialization)
alias(libs.plugins.download)
}
Expand Down Expand Up @@ -59,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"

Expand Down Expand Up @@ -107,25 +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 {
Expand Down
Binary file added app/src/main/resources/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/src/main/resources/welcome/intro/bubble.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions app/src/main/resources/welcome/intro/long.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions app/src/main/resources/welcome/intro/short.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions app/src/main/resources/welcome/intro/wavy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions app/src/processing/app/Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2190,10 +2191,11 @@ static private Mode findSketchMode(File folder, List<Mode> 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();
}


Expand Down
6 changes: 6 additions & 0 deletions app/src/processing/app/Language.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ static public Language init() {
return instance;
}

static public void reload(){
if(instance == null) return;
synchronized (Language.class) {
instance = new Language();
}
}

static private String get(String key) {
LanguageBundle bundle = init().bundle;
Expand Down
8 changes: 8 additions & 0 deletions app/src/processing/app/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
163 changes: 141 additions & 22 deletions app/src/processing/app/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>()

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<ReactiveProperties> { 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<WatchEvent<*>?> (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<Path> {
override fun count(): Int = 1
override fun context(): Path = file.toPath().fileName
override fun kind(): WatchEvent.Kind<Path> = StandardWatchEventKinds.ENTRY_MODIFY
override fun toString(): String = "ForcedEvent(${context()})"
}
continue
}else{
for (modified in key.pollEvents()) {
if (modified.context() != path.fileName) continue
event = modified
}
}
}
}
Expand All @@ -62,12 +189,4 @@ fun watchFile(file: File): Any? {
}
}
return event
}
val LocalPreferences = compositionLocalOf<Properties> { error("No preferences provided") }
@Composable
fun PreferencesProvider(content: @Composable () -> Unit){
val preferences = loadPreferences()
CompositionLocalProvider(LocalPreferences provides preferences){
content()
}
}
Loading