diff --git a/.all-contributorsrc b/.all-contributorsrc
index d1d18fb98..5ba97fcde 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1642,6 +1642,15 @@
"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 16e8984e3..8ec45cad0 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/README.md b/README.md
index c27391e6f..18920086f 100644
--- a/README.md
+++ b/README.md
@@ -313,6 +313,7 @@ _Note: due to GitHub's limitations, this repository's [Contributors](https://git
 Andrew 💻 |
 Ngoc Doan 💻 |
 Manoel Ribeiro 📖 |
+  Moon 💻 |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5323a1a82..c93092d59 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 000000000..4847f0a7c
--- /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 ce78b4b6c..06e6458fc 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -166,18 +166,6 @@ public class Base {
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 b911d7e0a..2c2ade5e1 100644
--- a/app/src/processing/app/Platform.java
+++ b/app/src/processing/app/Platform.java
@@ -105,6 +105,9 @@ public class Platform {
"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 @@ public class Platform {
}
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 @@ public class Platform {
}
}
+ // 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 4ca96d58e..a94f852df 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 1bfa29688..e18daee3e 100644
--- a/app/src/processing/app/UpdateCheck.java
+++ b/app/src/processing/app/UpdateCheck.java
@@ -35,6 +35,7 @@ import processing.app.ui.WelcomeToBeta;
import processing.core.PApplet;
+
/**
* Threaded class to check for updates in the background.
*
@@ -112,6 +113,7 @@ public class UpdateCheck {
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 class UpdateCheck {
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 000000000..25e693404
--- /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 000000000..0b57f369d
--- /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 000000000..d3fdb411b
--- /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 d7492fa6a..7757e820f 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 6e3e00f0d..1cfc190ca 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 70f93aaff..dfacae1ea 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" }