diff --git a/.all-contributorsrc b/.all-contributorsrc
index ec8fe5952a..5ba97fcdeb 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1624,6 +1624,33 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "pnngocdoan",
+ "name": "Ngoc Doan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/113954980?v=4",
+ "profile": "https://github.com/pnngocdoan",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "manoellribeiro",
+ "name": "Manoel Ribeiro",
+ "avatar_url": "https://avatars.githubusercontent.com/u/59377764?v=4",
+ "profile": "https://github.com/manoellribeiro",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "catilac",
+ "name": "Moon",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15107?v=4",
+ "profile": "https://softmoon.world",
+ "contributions": [
+ "code"
+ ]
}
],
"repoType": "github",
diff --git a/.github/workflows/release-gradle.yml b/.github/workflows/release-gradle.yml
index 16e8984e3b..8ec45cad0b 100644
--- a/.github/workflows/release-gradle.yml
+++ b/.github/workflows/release-gradle.yml
@@ -153,6 +153,7 @@ jobs:
ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.password: ${{ secrets.PROCESSING_APP_PASSWORD }}
ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.teamID: ${{ secrets.PROCESSING_TEAM_ID }}
ORG_GRADLE_PROJECT_snapname: ${{ vars.SNAP_NAME }}
+ ORG_GRADLE_PROJECT_snapconfinement: ${{ vars.SNAP_CONFINEMENT }}
- name: Sign files with Trusted Signing
if: runner.os == 'Windows'
diff --git a/BUILD.md b/BUILD.md
index a6b75b5678..a7176776a2 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -113,7 +113,7 @@ Processing consists of three main components: `Core`, `Java`, and `App`. The `Co
- If you've written a large sketch and Processing has become slow to compile and run it, a place to improve this code can most likely be found in the `Java` section.
## User interface
-Historically, Processing's UI has been written in Java Swing and Flatlaf (and some html & css). Since 2025 we have switched to include Jetpack Compose, mostly for it's backwards-compatibility with Swing. This approach allows us to gradually replace Java Swing components with Jetpack Compose ones, instead of doing a complete overhaul of the editor.
+Historically, Processing's UI has been written in Java Swing and Flatlaf (and some html & css). Since 2025 we have switched to include Jetpack Compose. It is backwards-compatible with Swing, which allows us to gradually replace Java Swing components with Jetpack Compose ones, instead of doing a complete overhaul of the editor.
## Build system
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f542aeef4b..ec957af698 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,12 +34,15 @@ Before beginning work on a code contribution, please make sure that:
- The issue has been discussed and a proposed solution has been approved.
- You have been **assigned** to the issue.
-
+
If an implementation has been agreed upon but no one has volunteered to take it on, feel free to comment and offer to help. A maintainer can then assign the issue to you.
+> [!NOTE]
+> If this is your first contribution to our repositories, or if an implementation has not been agreed upon yet, please include a brief explanation of how you plan to approach the issue. This helps us understand your thinking and gives us an opportunity to discuss the best solution. Note that **we do not auto-assign issues**, so comments that only say "please assign" without further context may be overlooked.
+
Please do **not** open a pull request for an issue that is already assigned to someone else. We follow a “first assigned, first served” approach to avoid duplicated work. If you open a PR for an issue that someone else is already working on, your PR will be closed.
-If an issue has been inactive for a long time, you’re welcome to check in politely by commenting to see if the assignee still plans to work on it or would be open to someone else taking over.
+If an issue has been inactive for over a month, you’re welcome to check in politely by commenting to see if the assignee still plans to work on it or would be open to someone else taking over.
There’s no hard deadline for completing contributions. We understand that people often contribute on a volunteer basis and timelines may vary. That said, if you run into trouble or have questions at any point, don’t hesitate to ask for help in the issue thread. Maintainers and other community members are here to support you.
@@ -51,6 +54,9 @@ Before you contribute your changes, it's essential that you make sure that Proce
## Submit a pull request (PR)
+> [!IMPORTANT]
+> Before submitting a pull request, please ask to be assigned to the corresponding issue. If someone else is already assigned or has shared that they’re working on it, we ask that you wait or choose another issue. This helps us avoid duplicated efforts and respect each other's time. PRs submitted without assignment may be closed without a review.
+
Once your changes are ready:
1. Push your branch to your fork
diff --git a/README.md b/README.md
index a854f0699b..18920086fa 100644
--- a/README.md
+++ b/README.md
@@ -311,6 +311,9 @@ _Note: due to GitHub's limitations, this repository's [Contributors](https://git
 Joackim de Bourqueney 💻 |
 Tonz 💻 📖 |
 Andrew 💻 |
+  Ngoc Doan 💻 |
+  Manoel Ribeiro 📖 |
+  Moon 💻 |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5323a1a829..c93092d595 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -124,6 +124,7 @@ dependencies {
testImplementation(libs.junitJupiterParams)
implementation(libs.clikt)
+ implementation(libs.kotlinxSerializationJson)
}
tasks.test {
@@ -228,61 +229,44 @@ tasks.register("packageCustomMsi"){
tasks.register("generateSnapConfiguration"){
- val name = findProperty("snapname") ?: rootProject.name
+ onlyIf { OperatingSystem.current().isLinux }
+
+ val distributable = tasks.named("createDistributable").get()
+ dependsOn(distributable)
+
+ val name = findProperty("snapname") as String? ?: rootProject.name
val arch = when (System.getProperty("os.arch")) {
"amd64", "x86_64" -> "amd64"
"aarch64" -> "arm64"
else -> System.getProperty("os.arch")
}
-
- onlyIf { OperatingSystem.current().isLinux }
- val distributable = tasks.named("createDistributable").get()
- dependsOn(distributable)
-
+ val confinement = findProperty("snapconfinement") as String? ?: "strict"
val dir = distributable.destinationDir.get()
- val content = """
- name: $name
- version: $version
- base: core22
- summary: A creative coding editor
- description: |
- Processing is a flexible software sketchbook and a programming language designed for learning how to code.
- confinement: strict
-
- apps:
- processing:
- command: opt/processing/bin/Processing
- desktop: opt/processing/lib/processing-Processing.desktop
- environment:
- LD_LIBRARY_PATH: ${'$'}SNAP/opt/processing/lib/runtime/lib:${'$'}LD_LIBRARY_PATH
- LIBGL_DRIVERS_PATH: ${'$'}SNAP/usr/lib/${'$'}SNAPCRAFT_ARCH_TRIPLET/dri
- plugs:
- - desktop
- - desktop-legacy
- - wayland
- - x11
- - network
- - opengl
- - home
- - removable-media
- - audio-playback
- - audio-record
- - pulseaudio
- - gpio
-
- parts:
- processing:
- plugin: dump
- source: deb/processing_$version-1_$arch.deb
- source-type: deb
- stage-packages:
- - openjdk-17-jre
- override-prime: |
- snapcraftctl prime
- rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts
- chmod -R +x opt/processing/lib/app/resources/jdk
- """.trimIndent()
- dir.file("../snapcraft.yaml").asFile.writeText(content)
+ val base = layout.projectDirectory.file("linux/snapcraft.base.yml")
+
+ doFirst {
+
+ var content = base
+ .asFile
+ .readText()
+ .replace("\$name", name)
+ .replace("\$arch", arch)
+ .replace("\$version", version as String)
+ .replace("\$confinement", confinement)
+ .let {
+ if (confinement != "classic") return@let it
+ // If confinement is not strict, remove the PLUGS section
+ val start = it.indexOf("# PLUGS START")
+ val end = it.indexOf("# PLUGS END")
+ if (start != -1 && end != -1) {
+ val before = it.substring(0, start)
+ val after = it.substring(end + "# PLUGS END".length)
+ return@let before + after
+ }
+ return@let it
+ }
+ dir.file("../snapcraft.yaml").asFile.writeText(content)
+ }
}
tasks.register("packageSnap"){
@@ -424,7 +408,6 @@ tasks.register("renameWindres") {
}
tasks.register("includeProcessingResources"){
dependsOn(
- "includeJdk",
"includeCore",
"includeJavaMode",
"includeSharedAssets",
@@ -433,6 +416,7 @@ tasks.register("includeProcessingResources"){
"includeJavaModeResources",
"renameWindres"
)
+ mustRunAfter("includeJdk")
finalizedBy("signResources")
}
@@ -539,6 +523,7 @@ afterEvaluate {
dependsOn("includeProcessingResources")
}
tasks.named("createDistributable").configure {
+ dependsOn("includeJdk")
finalizedBy("setExecutablePermissions")
}
}
diff --git a/app/linux/snapcraft.base.yml b/app/linux/snapcraft.base.yml
new file mode 100644
index 0000000000..4847f0a7c8
--- /dev/null
+++ b/app/linux/snapcraft.base.yml
@@ -0,0 +1,42 @@
+name: $name
+version: $version
+base: core22
+summary: A creative coding editor
+description: |
+ Processing is a flexible software sketchbook and a programming language designed for learning how to code.
+confinement: $confinement
+
+apps:
+ processing:
+ command: opt/processing/bin/Processing
+ desktop: opt/processing/lib/processing-Processing.desktop
+ environment:
+ LD_LIBRARY_PATH: $SNAP/opt/processing/lib/runtime/lib:$LD_LIBRARY_PATH
+ LIBGL_DRIVERS_PATH: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/dri
+ # PLUGS START
+ plugs:
+ - desktop
+ - desktop-legacy
+ - wayland
+ - x11
+ - network
+ - opengl
+ - home
+ - removable-media
+ - audio-playback
+ - audio-record
+ - pulseaudio
+ - gpio
+ # PLUGS END
+
+parts:
+ processing:
+ plugin: dump
+ source: deb/processing_$version-1_$arch.deb
+ source-type: deb
+ stage-packages:
+ - openjdk-17-jre
+ override-prime: |
+ snapcraftctl prime
+ rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts
+ chmod -R +x opt/processing/lib/app/resources/jdk
\ No newline at end of file
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index ce78b4b6cc..06e6458fc0 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -166,18 +166,6 @@ static public void main(final String[] args) {
static private void createAndShowGUI(String[] args) {
// these times are fairly negligible relative to Base.
// long t1 = System.currentTimeMillis();
- var preferences = java.util.prefs.Preferences.userRoot().node("org/processing/app");
- var installLocations = new ArrayList<>(List.of(preferences.get("installLocations", "").split(",")));
- var installLocation = System.getProperty("user.dir") + "^" + Base.getVersionName();
-
- // Check if the installLocation is already in the list
- if (!installLocations.contains(installLocation)) {
- // Add the installLocation to the list
- installLocations.add(installLocation);
-
- // Save the updated list back to preferences
- preferences.put("installLocations", String.join(",", installLocations));
- }
// TODO: Cleanup old locations if no longer installed
// TODO: Cleanup old locations if current version is installed in the same location
diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java
index b911d7e0ae..2c2ade5e12 100644
--- a/app/src/processing/app/Platform.java
+++ b/app/src/processing/app/Platform.java
@@ -105,6 +105,9 @@ static public void init() {
"An unknown error occurred while trying to load\n" +
"platform-specific code for your machine.", e);
}
+
+ // Fix the issue where `java.home` points to the JRE instead of the JDK. processing/processing4#1163
+ System.setProperty("java.home", getJavaHome().getAbsolutePath());
}
@@ -389,6 +392,7 @@ static public File getContentFile(String name) {
}
static public File getJavaHome() {
+ // Get the build in JDK location from the Jetpack Compose resources
var resourcesDir = System.getProperty("compose.application.resources.dir");
if(resourcesDir != null) {
var jdkFolder = new File(resourcesDir,"jdk");
@@ -397,10 +401,13 @@ static public File getJavaHome() {
}
}
+ // If the JDK is set in the environment, use that.
var home = System.getProperty("java.home");
if(home != null){
return new File(home);
}
+
+ // Otherwise try to use the Ant embedded JDK.
if (Platform.isMacOS()) {
//return "Contents/PlugIns/jdk1.7.0_40.jdk/Contents/Home/jre/bin/java";
File[] plugins = getContentFile("../PlugIns").listFiles((dir, name) -> dir.isDirectory() &&
diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt
index 4ca96d58ee..a94f852df8 100644
--- a/app/src/processing/app/Processing.kt
+++ b/app/src/processing/app/Processing.kt
@@ -10,7 +10,12 @@ import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
+import processing.app.api.Contributions
+import processing.app.api.Sketchbook
import processing.app.ui.Start
+import java.io.File
+import java.util.prefs.Preferences
+import kotlin.concurrent.thread
class Processing: SuspendingCliktCommand("processing"){
val version by option("-v","--version")
@@ -29,6 +34,11 @@ class Processing: SuspendingCliktCommand("processing"){
return
}
+ thread {
+ // Update the install locations in preferences
+ updateInstallLocations()
+ }
+
val subcommand = currentContext.invokedSubcommand
if (subcommand == null) {
Start.main(sketches.toTypedArray())
@@ -40,7 +50,9 @@ suspend fun main(args: Array){
Processing()
.subcommands(
LSP(),
- LegacyCLI(args)
+ LegacyCLI(args),
+ Contributions(),
+ Sketchbook()
)
.main(args)
}
@@ -49,6 +61,9 @@ class LSP: SuspendingCliktCommand("lsp"){
override fun help(context: Context) = "Start the Processing Language Server"
override suspend fun run(){
try {
+ // run in headless mode
+ System.setProperty("java.awt.headless", "true")
+
// Indirect invocation since app does not depend on java mode
Class.forName("processing.mode.java.lsp.PdeLanguageServer")
.getMethod("main", Array::class.java)
@@ -68,10 +83,9 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
override suspend fun run() {
try {
- if (arguments.contains("--build")) {
- System.setProperty("java.awt.headless", "true")
- }
+ System.setProperty("java.awt.headless", "true")
+ // Indirect invocation since app does not depend on java mode
Class.forName("processing.mode.java.Commander")
.getMethod("main", Array::class.java)
.invoke(null, arguments.toTypedArray())
@@ -80,3 +94,49 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
}
}
}
+
+fun updateInstallLocations(){
+ val preferences = Preferences.userRoot().node("org/processing/app")
+ val installLocations = preferences.get("installLocations", "")
+ .split(",")
+ .dropLastWhile { it.isEmpty() }
+ .filter { install ->
+ try{
+ val (path, version) = install.split("^")
+ val file = File(path)
+ if(!file.exists() || file.isDirectory){
+ return@filter false
+ }
+ // call the path to check if it is a valid install location
+ val process = ProcessBuilder(path, "--version")
+ .redirectErrorStream(true)
+ .start()
+ val exitCode = process.waitFor()
+ if(exitCode != 0){
+ return@filter false
+ }
+ val output = process.inputStream.bufferedReader().readText()
+ return@filter output.contains(version)
+ } catch (e: Exception){
+ false
+ }
+ }
+ .toMutableList()
+ val command = ProcessHandle.current().info().command()
+ if(command.isEmpty) {
+ return
+ }
+ val installLocation = "${command.get()}^${Base.getVersionName()}"
+
+
+ // Check if the installLocation is already in the list
+ if (installLocations.contains(installLocation)) {
+ return
+ }
+
+ // Add the installLocation to the list
+ installLocations.add(installLocation)
+
+ // Save the updated list back to preferences
+ preferences.put("installLocations", java.lang.String.join(",", installLocations))
+}
diff --git a/app/src/processing/app/UpdateCheck.java b/app/src/processing/app/UpdateCheck.java
index 1bfa296882..e18daee3eb 100644
--- a/app/src/processing/app/UpdateCheck.java
+++ b/app/src/processing/app/UpdateCheck.java
@@ -35,6 +35,7 @@
import processing.core.PApplet;
+
/**
* Threaded class to check for updates in the background.
*
@@ -112,6 +113,7 @@ public void updateCheck() throws IOException {
System.getProperty("os.arch"));
int latest = readInt(LATEST_URL + "?" + info);
+ int revision = Base.getRevision();
String lastString = Preferences.get("update.last");
long now = System.currentTimeMillis();
@@ -125,18 +127,19 @@ public void updateCheck() throws IOException {
Preferences.set("update.last", String.valueOf(now));
if (base.activeEditor != null) {
-// boolean offerToUpdateContributions = true;
- if (latest > Base.getRevision()) {
+ if (latest > revision) {
System.out.println("You are running Processing revision 0" +
- Base.getRevision() + ", the latest build is 0" +
+ revision + ", the latest build is 0" +
latest + ".");
// Assume the person is busy downloading the latest version
// offerToUpdateContributions = !promptToVisitDownloadPage();
promptToVisitDownloadPage();
}
- if(latest < Base.getRevision()){
- WelcomeToBeta.showWelcomeToBeta();
+
+ int lastBetaWelcomeSeen = Preferences.getInteger("update.beta_welcome");
+ if(latest < revision && revision != lastBetaWelcomeSeen ) {
+ WelcomeToBeta.showWelcomeToBeta();
}
/*
diff --git a/app/src/processing/app/api/Contributions.kt b/app/src/processing/app/api/Contributions.kt
new file mode 100644
index 0000000000..25e693404b
--- /dev/null
+++ b/app/src/processing/app/api/Contributions.kt
@@ -0,0 +1,144 @@
+package processing.app.api
+
+import com.github.ajalt.clikt.command.SuspendingCliktCommand
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.subcommands
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import processing.app.Platform
+import processing.app.api.Sketch.Companion.getSketches
+import java.io.File
+
+class Contributions: SuspendingCliktCommand(){
+ override fun help(context: Context) = "Manage Processing contributions"
+ override suspend fun run() {
+ System.setProperty("java.awt.headless", "true")
+ }
+ init {
+ subcommands(Examples())
+ }
+
+ class Examples: SuspendingCliktCommand("examples") {
+ override fun help(context: Context) = "Manage Processing examples"
+ override suspend fun run() {
+ }
+ init {
+ subcommands(ExamplesList())
+ }
+ }
+
+ class ExamplesList: SuspendingCliktCommand("list") {
+
+
+ val serializer = Json {
+ prettyPrint = true
+ }
+
+ 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)
+ println(json)
+ }
+
+ 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() }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/api/Sketch.kt b/app/src/processing/app/api/Sketch.kt
new file mode 100644
index 0000000000..0b57f369d9
--- /dev/null
+++ b/app/src/processing/app/api/Sketch.kt
@@ -0,0 +1,50 @@
+package processing.app.api
+
+import kotlinx.serialization.Serializable
+import java.io.File
+
+class Sketch {
+ companion object{
+ @Serializable
+ data class Sketch(
+ val type: String = "sketch",
+ val name: String,
+ val path: String,
+ val mode: String = "java",
+ )
+
+ @Serializable
+ data class Folder(
+ val type: String = "folder",
+ val name: String,
+ val path: String,
+ val mode: String = "java",
+ val children: List = emptyList(),
+ val sketches: List = emptyList()
+ )
+
+ fun getSketches(file: File, filter: (File) -> Boolean = { true }): Folder? {
+ val name = file.name
+ val (sketchesFolders, childrenFolders) = file.listFiles()?.filter (File::isDirectory)?.partition { isSketchFolder(it) } ?: return Folder(
+ name = name,
+ path = file.absolutePath,
+ sketches = emptyList(),
+ children = emptyList()
+ )
+ val children = childrenFolders.filter(filter).mapNotNull { getSketches(it) }
+ val sketches = sketchesFolders.map { Sketch(name = it.name, path = it.absolutePath) }
+ if(sketches.isEmpty() && children.isEmpty()) {
+ return null
+ }
+ return Folder(
+ name = name,
+ path = file.absolutePath,
+ children = children,
+ sketches = sketches
+ )
+ }
+ fun isSketchFolder(file: File): Boolean {
+ return file.isDirectory && file.listFiles().any { it.isFile && it.name.endsWith(".pde") }
+ }
+ }
+}
diff --git a/app/src/processing/app/api/Sketchbook.kt b/app/src/processing/app/api/Sketchbook.kt
new file mode 100644
index 0000000000..d3fdb411b3
--- /dev/null
+++ b/app/src/processing/app/api/Sketchbook.kt
@@ -0,0 +1,50 @@
+package processing.app.api
+
+import com.github.ajalt.clikt.command.SuspendingCliktCommand
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.subcommands
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import processing.app.Platform
+import processing.app.Preferences
+import processing.app.api.Sketch.Companion.getSketches
+import java.io.File
+
+class Sketchbook: SuspendingCliktCommand() {
+
+
+ override fun help(context: Context) = "Manage the sketchbook"
+ override suspend fun run() {
+ System.setProperty("java.awt.headless", "true")
+ }
+ init {
+ subcommands(SketchbookList())
+ }
+
+
+ class SketchbookList: SuspendingCliktCommand("list") {
+ val serializer = Json {
+ prettyPrint = true
+ }
+
+ override fun help(context: Context) = "List all sketches"
+ override suspend fun run() {
+ Platform.init()
+ // 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 sketches = getSketches(sketchbookFolder) {
+ !listOf(
+ "android",
+ "modes",
+ "tools",
+ "examples",
+ "libraries"
+ ).contains(it.name)
+ }
+ val json = serializer.encodeToString(listOf(sketches))
+ println(json)
+ }
+ }
+}
\ 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 d7492fa6aa..7757e820f6 100644
--- a/app/src/processing/app/ui/WelcomeToBeta.kt
+++ b/app/src/processing/app/ui/WelcomeToBeta.kt
@@ -35,6 +35,7 @@ import com.mikepenz.markdown.m2.markdownColor
import com.mikepenz.markdown.m2.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
@@ -61,7 +62,10 @@ class WelcomeToBeta {
val mac = SystemInfo.isMacFullWindowContentSupported
SwingUtilities.invokeLater {
JFrame(windowTitle).apply {
- val close = { dispose() }
+ 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
diff --git a/build/shared/lib/defaults.txt b/build/shared/lib/defaults.txt
index 6e3e00f0d6..1cfc190ca9 100644
--- a/build/shared/lib/defaults.txt
+++ b/build/shared/lib/defaults.txt
@@ -76,6 +76,10 @@ theme.gradient.method = rgb
# on how many people are using Processing)
update.check = true
+# default value for beta_welcome
+# -1 means no beta has been run
+update.beta_welcome = -1
+
# on windows, automatically associate .pde files with processing.exe
platform.auto_file_type_associations = true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 70f93aaff5..dfacae1ead 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,6 +28,7 @@ jsoup = { module = "org.jsoup:jsoup", version = "1.17.2" }
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" }
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
clikt = { module = "com.github.ajalt.clikt:clikt", version = "5.0.2" }
+kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" }
[plugins]
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }