diff --git a/.gitignore b/.gitignore
index 67057f9425..4532993a0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,19 @@ gen-external-apklibs
hs_err_pid*
replay_pid*
+# Maven ignores
+.kotlin
+.gradle
+.build/
+/core/build/
+/build/publish/
+/app/build
+/java/build/
+/build/reports
+/java/bin
+/java/libraries/svg/bin
+/java/preprocessor/build
+/java/lsp/build
### Gradle ###
.gradle
**/build/
@@ -123,4 +136,16 @@ generated/
!java/libraries/serial/library/jssc.jar
/app/windows/obj
/java/gradle/build
+/core/examples/build
/java/gradle/example/.processing
+/app/windows/obj
+/java/android/example/build
+/java/android/example/.processing
+/java/gradle/example/build
+/java/gradle/example/gradle/wrapper/gradle-wrapper.jar
+/java/gradle/example/gradle/wrapper/gradle-wrapper.properties
+/java/gradle/example/gradlew
+/java/gradle/example/gradlew.bat
+/java/gradle/example/.kotlin/errors
+/java/gradle/hotreload/build
+*.iml
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000000..2db2e88c86
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/ant/processing/app/gradle/GradleService.java b/app/ant/processing/app/gradle/GradleService.java
new file mode 100644
index 0000000000..98f7395133
--- /dev/null
+++ b/app/ant/processing/app/gradle/GradleService.java
@@ -0,0 +1,22 @@
+package processing.app.gradle;
+
+import processing.app.Mode;
+import processing.app.Sketch;
+import processing.app.ui.Editor;
+
+import java.io.PrintStream;
+
+public class GradleService {
+ public GradleService(Mode mode, Editor editor) { }
+
+ public void setEnabled(boolean enabled) {}
+ public boolean getEnabled() { return false; }
+ public void prepare(){}
+ public void run() {}
+ public void export(){}
+ public void stop() {}
+ public void startService() {}
+ public void setSketch(Sketch sketch) {}
+ public void setErr(PrintStream err) {}
+ public void setOut(PrintStream out) {}
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0d3fcbd12d..73ca3b2007 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,5 @@
import org.gradle.internal.jvm.Jvm
import org.gradle.internal.os.OperatingSystem
-import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
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
@@ -49,14 +48,18 @@ compose.desktop {
application {
mainClass = "processing.app.ProcessingKt"
- jvmArgs(*listOf(
- Pair("processing.version", rootProject.version),
- Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE),
- Pair("processing.contributions.source", "https://contributions.processing.org/contribs"),
- Pair("processing.download.page", "https://processing.org/download/"),
- Pair("processing.download.latest", "https://processing.org/download/latest.txt"),
- Pair("processing.tutorials", "https://processing.org/tutorials/"),
- ).map { "-D${it.first}=${it.second}" }.toTypedArray())
+
+ val variables = mapOf(
+ "processing.group" to (rootProject.group.takeIf { it != "" } ?: "processing"),
+ "processing.version" to rootProject.version,
+ "processing.revision" to (findProperty("revision") ?: Int.MAX_VALUE),
+ "processing.contributions.source" to "https://contributions.processing.org/contribs",
+ "processing.download.page" to "https://processing.org/download/",
+ "processing.download.latest" to "https://processing.org/download/latest.txt",
+ "processing.tutorials" to "https://processing.org/tutorials/"
+ )
+
+ jvmArgs(*variables.entries.map { "-D${it.key}=${it.value}" }.toTypedArray())
nativeDistributions{
modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting", "jdk.httpserver")
@@ -111,6 +114,7 @@ dependencies {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+ implementation(compose.materialIconsExtended)
implementation(compose.desktop.currentOs)
@@ -118,14 +122,14 @@ dependencies {
implementation(libs.kaml)
implementation(libs.markdown)
implementation(libs.markdownJVM)
+ implementation(gradleApi())
+ implementation(libs.clikt)
+ implementation(libs.kotlinxSerializationJson)
testImplementation(kotlin("test"))
testImplementation(libs.mockitoKotlin)
testImplementation(libs.junitJupiter)
testImplementation(libs.junitJupiterParams)
-
- implementation(libs.clikt)
- implementation(libs.kotlinxSerializationJson)
}
tasks.test {
@@ -315,7 +319,6 @@ afterEvaluate{
}
}
-
// LEGACY TASKS
// Most of these are shims to be compatible with the old build system
// They should be removed in the future, as we work towards making things more Gradle-native
@@ -390,23 +393,6 @@ tasks.register("includeJavaModeResources") {
from(java.layout.buildDirectory.dir("resources-bundled"))
into(composeResources("../"))
}
-// TODO: Move to java mode
-tasks.register("renameWindres") {
- dependsOn("includeSharedAssets","includeJavaModeResources")
- val dir = composeResources("modes/java/application/launch4j/bin/")
- val os = DefaultNativePlatform.getCurrentOperatingSystem()
- val platform = when {
- os.isWindows -> "windows"
- os.isMacOsX -> "macos"
- else -> "linux"
- }
- from(dir) {
- include("*-$platform*")
- rename("(.*)-$platform(.*)", "$1$2")
- }
- duplicatesStrategy = DuplicatesStrategy.INCLUDE
- into(dir)
-}
tasks.register("includeProcessingResources"){
dependsOn(
"includeCore",
@@ -414,8 +400,7 @@ tasks.register("includeProcessingResources"){
"includeSharedAssets",
"includeProcessingExamples",
"includeProcessingWebsiteExamples",
- "includeJavaModeResources",
- "renameWindres"
+ "includeJavaModeResources"
)
mustRunAfter("includeJdk")
finalizedBy("signResources")
@@ -495,9 +480,9 @@ tasks.register("signResources"){
}
file(composeResources("Info.plist")).delete()
}
+}
-}
tasks.register("setExecutablePermissions") {
description = "Sets executable permissions on binaries in Processing.app resources"
group = "compose desktop"
@@ -522,6 +507,8 @@ tasks.register("setExecutablePermissions") {
afterEvaluate {
tasks.named("prepareAppResources").configure {
dependsOn("includeProcessingResources")
+ // Make sure all libraries are bundled in the maven repository distributed with the app
+ dependsOn(listOf("app:utils","core","java:preprocessor", "java:gradle", "java:gradle:hotreload").map { project(":$it").tasks.named("publishAllPublicationsToAppRepository") })
}
tasks.named("createDistributable").configure {
dependsOn("includeJdk")
diff --git a/app/src/main/resources/defaults.txt b/app/src/main/resources/defaults.txt
index 6e3e00f0d6..431988bb74 100644
--- a/app/src/main/resources/defaults.txt
+++ b/app/src/main/resources/defaults.txt
@@ -186,6 +186,9 @@ console.temp.days = 7
console.scrollback.lines = 500
console.scrollback.chars = 40000
+# run java sketches with Gradle aka the Modern Build System
+run.use_gradle = false
+
# Any additional Java options when running.
# If you change this and can't run things, it's your own durn fault.
run.options =
diff --git a/app/src/processing/app/Language.java b/app/src/processing/app/Language.java
index d55c8b710c..ad67ffe8fb 100644
--- a/app/src/processing/app/Language.java
+++ b/app/src/processing/app/Language.java
@@ -183,7 +183,6 @@ static public Language init() {
return instance;
}
-
static private String get(String key) {
LanguageBundle bundle = init().bundle;
diff --git a/app/src/processing/app/Preferences.java b/app/src/processing/app/Preferences.java
index 640c77eade..076506296f 100644
--- a/app/src/processing/app/Preferences.java
+++ b/app/src/processing/app/Preferences.java
@@ -393,6 +393,8 @@ static public String getSketchbookPath() {
static protected void setSketchbookPath(String path) {
+ // Unify path seperator for all platforms
+ path = path.replace(File.separatorChar, '/');
set("sketchbook.path.four", path); //$NON-NLS-1$
}
}
diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt
index c5645c9bbc..87f36fe358 100644
--- a/app/src/processing/app/Preferences.kt
+++ b/app/src/processing/app/Preferences.kt
@@ -34,6 +34,7 @@ fun loadPreferences(): Properties{
}
}
+// TODO: Move this to a more appropriate place
@Composable
fun watchFile(file: File): Any? {
val scope = rememberCoroutineScope()
diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt
index 6bc6b64a7e..912f427213 100644
--- a/app/src/processing/app/Processing.kt
+++ b/app/src/processing/app/Processing.kt
@@ -10,6 +10,7 @@ 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.gradle.api.Sketch
import processing.app.api.Contributions
import processing.app.api.SketchCommand
import processing.app.api.Sketchbook
@@ -19,8 +20,7 @@ import java.util.prefs.Preferences
import kotlin.concurrent.thread
-
-suspend fun main(args: Array){
+suspend fun main(args: Array) {
Processing()
.subcommands(
LSP(),
@@ -32,8 +32,8 @@ suspend fun main(args: Array){
.main(args)
}
-class Processing: SuspendingCliktCommand("processing"){
- val version by option("-v","--version")
+class Processing : SuspendingCliktCommand("processing") {
+ val version by option("-v", "--version")
.flag()
.help("Print version information")
@@ -44,7 +44,7 @@ class Processing: SuspendingCliktCommand("processing"){
override fun help(context: Context) = "Start the Processing IDE"
override val invokeWithoutSubcommand = true
override suspend fun run() {
- if(version){
+ if (version) {
println("processing-${Base.getVersionName()}-${Base.getRevision()}")
return
}
@@ -62,9 +62,9 @@ class Processing: SuspendingCliktCommand("processing"){
}
-class LSP: SuspendingCliktCommand("lsp"){
+class LSP : SuspendingCliktCommand("lsp") {
override fun help(context: Context) = "Start the Processing Language Server"
- override suspend fun run(){
+ override suspend fun run() {
try {
// run in headless mode
System.setProperty("java.awt.headless", "true")
@@ -79,7 +79,7 @@ class LSP: SuspendingCliktCommand("lsp"){
}
}
-class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
+class LegacyCLI(val args: Array) : SuspendingCliktCommand("cli") {
override val treatUnknownOptionsAsArgs = true
val help by option("--help").flag()
@@ -99,16 +99,16 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") {
}
}
-fun updateInstallLocations(){
+fun updateInstallLocations() {
val preferences = Preferences.userRoot().node("org/processing/app")
val installLocations = preferences.get("installLocations", "")
.split(",")
.dropLastWhile { it.isEmpty() }
.filter { install ->
- try{
+ try {
val (path, version) = install.split("^")
val file = File(path)
- if(!file.exists() || file.isDirectory){
+ if (!file.exists() || file.isDirectory) {
return@filter false
}
// call the path to check if it is a valid install location
@@ -116,18 +116,18 @@ fun updateInstallLocations(){
.redirectErrorStream(true)
.start()
val exitCode = process.waitFor()
- if(exitCode != 0){
+ if (exitCode != 0) {
return@filter false
}
val output = process.inputStream.bufferedReader().readText()
return@filter output.contains(version)
- } catch (e: Exception){
+ } catch (e: Exception) {
false
}
}
.toMutableList()
val command = ProcessHandle.current().info().command()
- if(command.isEmpty) {
+ if (command.isEmpty) {
return
}
val installLocation = "${command.get()}^${Base.getVersionName()}"
diff --git a/app/src/processing/app/Sketch.java b/app/src/processing/app/Sketch.java
index 8bb50352b0..52c0de5b20 100644
--- a/app/src/processing/app/Sketch.java
+++ b/app/src/processing/app/Sketch.java
@@ -50,6 +50,8 @@
* Stores information about files in the current sketch.
*/
public class Sketch {
+ public static final String PROPERTIES_NAME = "sketch.properties";
+
private final Editor editor;
private final Mode mode;
@@ -1305,7 +1307,7 @@ static protected Settings loadProperties(File folder) throws IOException {
}
return null;
*/
- return new Settings(new File(folder, "sketch.properties"));
+ return new Settings(new File(folder, PROPERTIES_NAME));
}
diff --git a/app/src/processing/app/gradle/Debugger.kt b/app/src/processing/app/gradle/Debugger.kt
new file mode 100644
index 0000000000..9c93bd824a
--- /dev/null
+++ b/app/src/processing/app/gradle/Debugger.kt
@@ -0,0 +1,44 @@
+package processing.app.gradle
+
+import com.sun.jdi.Bootstrap
+import com.sun.jdi.VirtualMachine
+import com.sun.jdi.connect.AttachingConnector
+import kotlinx.coroutines.delay
+import processing.app.Messages
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.TimeSource
+
+class Debugger {
+ companion object {
+ suspend fun connect(port: Int?): VirtualMachine? {
+ try {
+ Messages.log("Attaching to VM $port")
+ val connector = Bootstrap.virtualMachineManager().allConnectors()
+ .firstOrNull { it.name() == "com.sun.jdi.SocketAttach" }
+ as AttachingConnector?
+ ?: throw IllegalStateException("No socket attach connector found")
+ val args = connector.defaultArguments()
+ args["port"]?.setValue(port?.toString() ?: "5005")
+
+ // Try to attach the debugger, retrying if it fails
+ // TODO: Stop retrying after the job has been cancelled / failed
+ val start = TimeSource.Monotonic.markNow()
+ while (start.elapsedNow() < 10.seconds) {
+ try {
+ val sketch = connector.attach(args)
+ sketch.resume()
+ Messages.log("Attached to VM: ${sketch.name()}")
+ return sketch
+ } catch (e: Exception) {
+ Messages.log("Error while attaching to VM: ${e.message}... Retrying")
+ }
+ delay(250)
+ }
+ } catch (e: Exception) {
+ Messages.log("Error while attaching to VM: ${e.message}")
+ return null
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/Exceptions.kt b/app/src/processing/app/gradle/Exceptions.kt
new file mode 100644
index 0000000000..c913ef4f8b
--- /dev/null
+++ b/app/src/processing/app/gradle/Exceptions.kt
@@ -0,0 +1,99 @@
+package processing.app.gradle
+
+import com.sun.jdi.Location
+import com.sun.jdi.StackFrame
+import com.sun.jdi.VirtualMachine
+import com.sun.jdi.event.ExceptionEvent
+import com.sun.jdi.request.EventRequest
+import kotlinx.coroutines.delay
+import processing.app.Messages
+import processing.app.SketchException
+import processing.app.ui.Editor
+
+// TODO: Consider adding a panel to the footer
+class Exceptions (val vm: VirtualMachine, val editor: Editor?) {
+ suspend fun listen() {
+ try {
+ val manager = vm.eventRequestManager()
+
+ val request = manager.createExceptionRequest(null, false, true)
+ request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD)
+ request.enable()
+
+ val queue = vm.eventQueue()
+ while (true) {
+ val eventSet = queue.remove()
+ for (event in eventSet) {
+ if (event is ExceptionEvent) {
+ printExceptionDetails(event)
+ event.thread().resume()
+ }
+ }
+ eventSet.resume()
+ delay(10)
+ }
+ } catch (e: Exception) {
+ Messages.log("Error while listening for exceptions: ${e.message}")
+ }
+ }
+
+ fun printExceptionDetails(event: ExceptionEvent) {
+ val exception = event.exception()
+ val thread = event.thread()
+ val location = event.location().mapToPdeFile()
+ val stackFrames = thread.frames()
+
+ val (processingFrames, userFrames) = stackFrames
+ .map{
+ val location = it.location().mapToPdeFile()
+ val method = location.method()
+ it to "${method.declaringType().name()}.${method.name()}() @ ${location.sourcePath()}:${location.lineNumber()}"
+ }
+ .partition {
+ it.first.location().declaringType().name().startsWith("processing.")
+ }
+
+ /*
+ We have 6 lines by default within the editor to display more information about the exception.
+ */
+ // TODO: Improve the display and clarity of the exception details
+
+ val message = """
+ In Processing code:
+ #processingFrames
+
+ In your code:
+ #userFrames
+
+ """
+ .trimIndent()
+ .replace("#processingFrames", processingFrames.joinToString("\n ") { it.second })
+ .replace("#userFrames", userFrames.joinToString("\n ") { it.second })
+
+ val error = """
+ Exception: ${exception.referenceType().name()} @ ${location.sourcePath()}:${location.lineNumber()}
+ """.trimIndent()
+
+ println(message)
+ System.err.println(error)
+
+ editor?.statusError(exception.referenceType().name())
+ }
+
+ fun Location.mapToPdeFile(): Location {
+ if(editor == null) return this
+
+ // Check if the source is a .java file
+ val sketch = editor.sketch
+ sketch.code.forEach { code ->
+ if(code.extension != "java") return@forEach
+ if(sourceName() != code.fileName) return@forEach
+ return@mapToPdeFile this
+ }
+
+ // TODO: Map to .pde file again, @see JavaBuild.placeException
+ // TODO: This functionality should be provided by the mode
+
+ return this
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/GradleJob.kt b/app/src/processing/app/gradle/GradleJob.kt
new file mode 100644
index 0000000000..a2baeada6a
--- /dev/null
+++ b/app/src/processing/app/gradle/GradleJob.kt
@@ -0,0 +1,445 @@
+package processing.app.gradle
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import com.sun.jdi.VirtualMachine
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.gradle.tooling.BuildCancelledException
+import org.gradle.tooling.BuildLauncher
+import org.gradle.tooling.GradleConnector
+import org.gradle.tooling.events.ProgressListener
+import org.gradle.tooling.events.problems.ProblemEvent
+import org.gradle.tooling.events.problems.Severity
+import org.gradle.tooling.events.problems.internal.DefaultSingleProblemEvent
+import org.gradle.tooling.events.task.TaskFinishEvent
+import org.gradle.tooling.events.task.TaskStartEvent
+import org.gradle.tooling.events.task.TaskSuccessResult
+import processing.app.Base.DEBUG
+import processing.app.Base.getSketchbookFolder
+import processing.app.Base.getVersionName
+import processing.app.Language.text
+import processing.app.Messages
+import processing.app.Platform
+import processing.app.Platform.getContentFile
+import processing.app.Platform.getSettingsFolder
+import processing.app.Settings
+import processing.app.Sketch
+import processing.app.gradle.Log.Companion.startLogServer
+import processing.app.ui.Editor
+import processing.app.ui.EditorStatus
+import java.nio.file.Path
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.writeText
+import kotlin.text.split
+
+/*
+* The gradle job runs the gradle tasks and manages the gradle connection
+ */
+class GradleJob(
+ vararg val tasks: String,
+ val workingDir: Path,
+ val sketch: Sketch,
+ val editor: Editor? = null,
+){
+ enum class State{
+ NONE,
+ BUILDING,
+ RUNNING,
+ ERROR,
+ DONE
+ }
+
+ val debugPort = (30_000..60_000).random()
+ val logPort = debugPort + 1
+ val errPort = logPort + 1
+
+ val state = mutableStateOf(State.NONE)
+ val vm = mutableStateOf(null)
+ val problems = mutableStateListOf()
+ val jobs = mutableStateListOf()
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private val cancel = GradleConnector.newCancellationTokenSource()
+
+
+ /*
+ Set up the gradle build launcher with the necessary configuration
+ This includes setting the working directory, the tasks to run,
+ and the arguments to pass to gradle.
+ Create the necessary build files if they do not exist.
+ */
+ private fun BuildLauncher.setupGradle(extraArguments: List = listOf()) {
+
+ val copy = sketch.isReadOnly || sketch.isUntitled
+
+ val sketchFolder = if(copy) workingDir.resolve("sketch").toFile() else sketch.folder
+
+ if(copy){
+ // If the sketch is read-only, we copy it to the working directory
+ // This allows us to run the sketch without modifying the original files
+ sketch.folder.copyRecursively(sketchFolder, overwrite = true)
+ }
+ // Save the unsaved code into the working directory for gradle to compile
+ val unsaved = sketch.code
+ .map { code ->
+ val file = workingDir.resolve("unsaved/${code.fileName}")
+ file.parent.toFile().mkdirs()
+ // If tab is marked modified save it to the working directory
+ // Otherwise delete the file so we don't compile with old code
+ if(code.isModified){
+ file.writeText(code.documentText)
+ }else{
+ file.deleteIfExists()
+ }
+ return@map code.fileName
+ }
+ // Collect the variables to pass to gradle
+ val variables = mapOf(
+ "group" to System.getProperty("processing.group", "org.processing"),
+ "version" to getVersionName(),
+ "sketchFolder" to sketchFolder,
+ "sketchbook" to getSketchbookFolder(),
+ "root" to getContentFile("").absolutePath.replace("""\""", """\\"""),
+ "workingDir" to workingDir.toAbsolutePath().toString(),
+ "settings" to getSettingsFolder().absolutePath.toString(),
+ "unsaved" to unsaved.joinToString(","),
+ "debugPort" to debugPort.toString(),
+ "logPort" to logPort.toString(),
+ "errPort" to errPort.toString(),
+ "fullscreen" to System.getProperty("processing.fullscreen", "false").equals("true"),
+ "display" to 1, // TODO: Implement
+ "external" to true,
+ "location" to null, // TODO: Implement
+ "editor.location" to editor?.location?.let { "${it.x},${it.y}" },
+ //"awt.disable" to false,
+ //"window.color" to "0xFF000000", // TODO: Implement
+ //"stop.color" to "0xFF000000", // TODO: Implement
+ "stop.hide" to false, // TODO: Implement
+ )
+ val repository = getContentFile("repository").absolutePath.replace("""\""", """\\""")
+ // Create the init.gradle.kts file in the working directory
+ // This allows us to run the gradle plugin that has been bundled with the editor
+ // TODO: Add the plugin repositories if they are defined
+ // TODO: Feedback when the JDK is being downloaded
+ val initGradle = workingDir.resolve("init.gradle.kts").apply {
+ val content = """
+ beforeSettings{
+ pluginManagement {
+ plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+ }
+ repositories {
+ maven("$repository")
+ gradlePluginPortal()
+ }
+ }
+ }
+ allprojects{
+ repositories {
+ maven("$repository")
+ mavenCentral()
+ }
+ }
+ """.trimIndent()
+
+ writeText(content)
+ }
+ // Create the build.gradle.kts file in the sketch folder
+ val buildGradle = sketchFolder.resolve("build.gradle.kts")
+ val generate = buildGradle.let {
+ if(!it.exists()) return@let true
+
+ val contents = it.readText()
+ if(!contents.contains("@processing-auto-generated")) return@let false
+
+ val version = contents.substringAfter("version=").substringBefore("\n")
+ if(version != getVersionName()) return@let true
+
+ val modeTitle = contents.substringAfter("mode=").substringBefore(" ")
+ if(sketch.mode.title != modeTitle) return@let true
+
+ return@let DEBUG
+ }
+ if (generate) {
+ Messages.log("build.gradle.kts outdated or not found in ${sketch.folder}, creating one")
+ val header = """
+ // @processing-auto-generated mode=${sketch.mode.title} version=${getVersionName()}
+ //
+ """.trimIndent()
+
+ val instructions = text("gradle.instructions")
+ .split("\n")
+ .joinToString("\n") { "// $it" }
+
+ val enabledPlugins = mutableListOf(GradlePlugin(
+ "Processing Java",
+ "The Processing Java mode for Gradle",
+ null,
+ "org.processing.java",
+ getVersionName()
+ ))
+ val propertiesFile = sketchFolder.resolve(Sketch.PROPERTIES_NAME)
+ if(propertiesFile.exists()){
+ val sketchSettings = Settings(propertiesFile)
+
+ // Grab the installed plugins
+ val plugins = GradlePlugin.plugins
+
+ // Grab the enabled plugins
+ val pluginSetting = (sketchSettings.get(GradlePlugin.PROPERTIES_KEY) ?: "")
+ .split(",")
+ .map { it.trim() }
+ .filter{ it.isNotEmpty() }
+
+ // Link plugins in the settings to their installed counterparts
+ enabledPlugins.addAll(
+ pluginSetting
+ .mapNotNull { id ->
+ plugins.find { plugin -> plugin.id == id
+ }
+ }
+ )
+ }
+
+ val pluginList = enabledPlugins
+ .joinToString("\n ") { "id(\"${it.id}\") version \"${it.version}\"" }
+
+ val configuration = """
+ plugins{
+ #plugins
+ }
+ """.trimIndent().replace("#plugins", pluginList)
+ val content = "${header}\n${instructions}\n\n${configuration}"
+ buildGradle.writeText(content)
+ }
+ // Create and empty settings.gradle.kts file in the sketch folder
+ val settingsGradle = sketchFolder.resolve("settings.gradle.kts")
+ if (!settingsGradle.exists()) {
+ settingsGradle.createNewFile()
+ }
+ // Collect the arguments to pass to gradle
+ val arguments = mutableListOf("--init-script", initGradle.toAbsolutePath().toString())
+ // Hide Gradle output from the console if not in debug mode
+ if(!DEBUG) arguments += "--quiet"
+ if(copy) arguments += listOf("--project-dir", sketchFolder.absolutePath)
+
+ arguments += variables.entries
+ .filter { it.value != null }
+ .map { "-Pprocessing.${it.key}=${it.value}" }
+
+ arguments += extraArguments
+
+ withArguments(*arguments.toTypedArray())
+
+ forTasks(*tasks)
+
+ // TODO: Instead of shipping Processing with a build-in JDK we should download the JDK through Gradle
+ setJavaHome(Platform.getJavaHome())
+ withCancellationToken(cancel.token())
+ }
+
+ /*
+ Start the gradle job and run the tasks
+ */
+ fun start() {
+ launchJob {
+ handleExceptions {
+ state.value = State.BUILDING
+
+ // Connect Gradle, configure the build and run it
+ GradleConnector.newConnector()
+ .forProjectDirectory(sketch.folder)
+ .apply {
+ editor?.statusMessage("Connecting to Gradle", EditorStatus.NOTICE)
+ // TODO: Remove when switched to classic confinement within Snap
+ if (System.getenv("SNAP_USER_COMMON") != null) {
+ useGradleUserHomeDir(getSettingsFolder().resolve("gradle"))
+ }
+ }
+ .connect()
+ .apply {
+ editor?.statusMessage("Building sketch", EditorStatus.NOTICE)
+ }
+ .newBuild()
+ .apply {
+ if (DEBUG) {
+ setStandardOutput(System.out)
+ setStandardError(System.err)
+ }
+
+ setupGradle()
+
+ addStateListener()
+ addLogserver()
+ addDebugging()
+
+ }
+ .run()
+ }
+ }
+ }
+
+
+ /*
+ Cancel the gradle job and all the jobs that were launched in this scope
+ */
+ fun cancel(){
+ cancel.cancel()
+ jobs.forEach(Job::cancel)
+ }
+
+ /*
+ Add a job to the scope and add it to the list of jobs so we can cancel it later
+ */
+ private fun launchJob(block: suspend CoroutineScope.() -> Unit){
+ val job = scope.launch { block() }
+ jobs.add(job)
+ }
+
+
+ /*
+ Handle exceptions that occur during the build process and inform the user about them
+ */
+ private fun handleExceptions(action: () -> Unit){
+ try{
+ action()
+ }catch (e: Exception){
+ val causesList = mutableListOf()
+ var cause: Throwable? = e
+
+ while (cause != null && cause.cause != cause) {
+ causesList.add(cause)
+ cause = cause.cause
+ }
+
+ val errors = causesList.joinToString("\n") { it.message ?: "Unknown error" }
+
+ val skip = listOf(BuildCancelledException::class)
+
+ if (skip.any { it.isInstance(e) }) {
+ Messages.log("Gradle job error: $errors")
+ return
+ }
+
+ if(state.value == State.RUNNING){
+ Messages.log("Gradle job error: $errors")
+ return
+ }
+
+ // An error occurred during the build process
+
+ System.err.println(errors)
+ editor?.statusError(causesList.last().message)
+ }finally {
+ state.value = State.DONE
+ vm.value = null
+ }
+ }
+
+ // TODO: Move to separate file?
+ /*
+ Add a progress listener to the build launcher
+ to track the progress of the build and update the editor status accordingly
+ */
+ private fun BuildLauncher.addStateListener(){
+ addProgressListener(ProgressListener { event ->
+ if(event is TaskStartEvent) {
+ editor?.statusMessage("Running task: ${event.descriptor.name}", EditorStatus.NOTICE)
+ when(event.descriptor.name) {
+ ":run" -> {
+ state.value = State.RUNNING
+ Messages.log("Start run")
+ editor?.toolbar?.activateRun()
+ }
+ }
+
+ }
+ if(event is TaskFinishEvent) {
+ if(event.result is TaskSuccessResult){
+ editor?.statusMessage("Finished task ${event.descriptor.name}", EditorStatus.NOTICE)
+ }
+
+ when(event.descriptor.name){
+ ":run"->{
+ state.value = State.DONE
+ editor?.toolbar?.deactivateRun()
+ editor?.toolbar?.deactivateStop()
+ }
+ }
+ }
+ if(event is DefaultSingleProblemEvent) {
+
+
+ problems.add(event)
+
+ val skip = listOf(
+ "mutating-the-dependencies-of-configuration-implementation-after-it-has-been-resolved-or-consumed-this-behavior-has-been-deprecated",
+ "mutating-the-dependencies-of-configuration-runtimeonly-after-it-has-been-resolved-or-consumed-this-behavior-has-been-deprecated"
+ )
+ if(skip.any { event.definition.id.name.contains(it) }) {
+ Messages.log(event.toString())
+ return@ProgressListener
+ }
+
+ if(event.definition.severity == Severity.ADVICE) {
+ Messages.log(event.toString())
+ return@ProgressListener
+ }
+ // TODO: Show the error on the location if it is available
+ // TODO: This functionality should be provided by the mode
+ /*
+ We have 6 lines to display the error in the editor.
+ */
+
+ val error = event.definition.id.displayName
+ editor?.statusError(error)
+ System.err.println("Problem: $error")
+ state.value = State.ERROR
+
+ val message = """
+ Context: ${event.contextualLabel.contextualLabel}
+ Solutions: ${event.solutions.joinToString("\n\t") { it.solution }}
+ """
+ .trimIndent()
+
+ println(message)
+ }
+ })
+ }
+
+ /*
+ Start log servers for the standard output and error streams
+ This allows us to capture the output of Processing and display it in the editor
+ Whilst keeping the gradle output separate
+ */
+ fun BuildLauncher.addLogserver(){
+ launchJob {
+ startLogServer(logPort, System.out)
+ }
+ launchJob{
+ startLogServer(errPort, System.err)
+ }
+ }
+
+ /*
+ Connected a debugger to the gradle run task
+ This allows us to debug the sketch while it is running
+ */
+ fun BuildLauncher.addDebugging() {
+ addProgressListener(ProgressListener { event ->
+ if (event !is TaskStartEvent) return@ProgressListener
+ if (event.descriptor.name != ":run") return@ProgressListener
+
+ launchJob {
+ val debugger = Debugger.connect(debugPort) ?: return@launchJob
+ vm.value = debugger
+ val exceptions = Exceptions(debugger, editor)
+ exceptions.listen()
+ }
+
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/GradlePlugin.kt b/app/src/processing/app/gradle/GradlePlugin.kt
new file mode 100644
index 0000000000..798dd97936
--- /dev/null
+++ b/app/src/processing/app/gradle/GradlePlugin.kt
@@ -0,0 +1,20 @@
+package processing.app.gradle
+
+import androidx.compose.runtime.mutableStateListOf
+import processing.app.Base
+import java.nio.file.Path
+
+data class GradlePlugin(
+ val name: String,
+ val description: String,
+ val repository: Path?,
+ val id: String,
+ val version: String){
+ companion object{
+ const val PROPERTIES_KEY = "sketch.plugins"
+ val plugins = mutableStateListOf(
+ GradlePlugin("Hot Reload", "Automatically apply changes in your sketch upon saving", null, "org.processing.java.hotreload", Base.getVersionName()),
+ GradlePlugin("Android","Run your sketch on an Android device", null, "org.processing.android", Base.getVersionName()),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/GradleService.kt b/app/src/processing/app/gradle/GradleService.kt
new file mode 100644
index 0000000000..34d0498a27
--- /dev/null
+++ b/app/src/processing/app/gradle/GradleService.kt
@@ -0,0 +1,121 @@
+package processing.app.gradle
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.ui.awt.ComposePanel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import processing.app.Language.text
+import processing.app.Mode
+import processing.app.Preferences
+import processing.app.Sketch
+import processing.app.ui.Editor
+import processing.app.ui.Theme
+import kotlin.io.path.createTempDirectory
+
+// TODO: Highlight errors in the editor in the right place
+
+// TODO: ---- FUTURE ----
+// TODO: Improve progress tracking and show it in the UI
+// TODO: PoC new debugger/tweak mode
+// TODO: Track build speed (for analytics?)
+// TODO: Bundle Gradle with the app
+
+/*
+* The gradle service runs the gradle tasks and manages the gradle connection
+* It will create the necessary build files for gradle to run
+* Then it will kick off a new GradleJob to run the tasks
+* GradleJob manages the gradle build and connects the debugger
+*/
+class GradleService(
+ val mode: Mode,
+ val editor: Editor?,
+) {
+ val active = mutableStateOf(Preferences.getBoolean("run.use_gradle"))
+ var sketch = mutableStateOf(null, neverEqualPolicy())
+ val jobs = mutableStateListOf()
+ val workingDir = createTempDirectory()
+
+ fun run(){
+ startJob("run")
+ }
+
+ fun export(){
+ startJob("runDistributable")
+ }
+
+ fun stop(){
+ stopJobs()
+ }
+
+ private fun startJob(vararg tasks: String) {
+ if(!active.value) return
+ editor?.let { println(text("gradle.using_gradle")) }
+
+ val job = GradleJob(
+ tasks = tasks,
+ workingDir = workingDir,
+ sketch = sketch.value ?: throw IllegalStateException("Sketch is not set"),
+ editor = editor
+ )
+ jobs.add(job)
+ job.start()
+ }
+
+ private fun stopJobs(){
+ jobs.forEach(GradleJob::cancel)
+ }
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ /*
+ Watch the sketch folder for changes and start a build job when the sketch is modified
+ This need to be done properly to use hooks in the future but right now this is the simplest way to do it
+ */
+ init{
+ scope.launch {
+ var path = ""
+ var modified = false
+ var sketched: Sketch? = null
+ while(true){
+ sketch.value?.let { sketch ->
+ if(sketch.folder.absolutePath != path){
+ path = sketch.folder.absolutePath
+ if(sketched == sketch){
+ // The same sketch has its folder changed, trigger updates downstream from the service
+ this@GradleService.sketch.value = sketch
+ }else {
+ sketched = sketch
+ }
+ startJob("build")
+ }
+ if(sketch.isModified != modified){
+ modified = sketch.isModified
+ if(!modified){
+ // If the sketch is no longer modified, start the build job, aka build on save
+ startJob("build")
+ }
+ }
+ }
+
+
+ delay(100)
+ }
+ }
+ }
+
+ // Hooks for java to interact with the Gradle service since mutableStateOf is not accessible in java
+ fun setSketch(sketch: Sketch){
+ this.sketch.value = sketch
+ }
+ fun getEnabled(): Boolean {
+ return active.value
+ }
+ fun setEnabled(active: Boolean) {
+ if(!active) stopJobs()
+ this.active.value = active
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/GradleSettings.kt b/app/src/processing/app/gradle/GradleSettings.kt
new file mode 100644
index 0000000000..8eebd7699d
--- /dev/null
+++ b/app/src/processing/app/gradle/GradleSettings.kt
@@ -0,0 +1,195 @@
+package processing.app.gradle
+
+import androidx.compose.foundation.VerticalScrollbar
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.scrollable
+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.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.rememberScrollbarAdapter
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Checkbox
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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.awt.ComposePanel
+import androidx.compose.ui.awt.SwingPanel
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import com.formdev.flatlaf.util.SwingUtils
+import com.github.ajalt.mordant.rendering.TextStyle
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import processing.app.Language.text
+import processing.app.Settings
+import processing.app.Sketch
+import processing.app.ui.Editor
+import processing.app.ui.EditorFooter
+import processing.app.ui.Theme
+import processing.app.ui.theme.ProcessingTheme
+import processing.app.watchFile
+import java.awt.Dimension
+import java.util.UUID
+import javax.swing.JCheckBox
+import javax.swing.JPanel
+import javax.swing.SwingUtilities
+
+class GradleSettings{
+ companion object{
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ @JvmStatic
+ fun addGradleSettings(footer: EditorFooter, service: GradleService){
+ val panel = ComposePanel()
+ panel.setContent {
+ Panel(service)
+ }
+ scope.launch {
+ // Only add the panel to the footer when Gradle is active
+ // Can be removed later when Gradle becomes the default build system
+ snapshotFlow { service.active.value }
+ .collect { active ->
+ SwingUtilities.invokeLater {
+ if(active){
+ footer.addPanel(panel, text("gradle.settings"), "/lib/footer/settings")
+ }else{
+ footer.removePanel(panel)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun Panel(service: GradleService){
+ val properties = service.sketch.value?.folder?.resolve(Sketch.PROPERTIES_NAME) ?: return
+ // TODO: Rewatch again is the sketch is saved in a different location
+
+ val changed = watchFile(properties)
+
+ val settings = remember(changed) {Settings(properties) }
+
+ LaunchedEffect(changed){
+ /*
+ If the sketch.id is not set, generate a new UUID and save it.
+ We will use this key to save preferences that do not influence the sketch itself,
+ so they are not code, but do influence how the sketch shows up in the editor.
+ This is useful for things like favoring a sketch
+ These are items that should not be shared between users/computers
+ // TODO: Reset id on save-as?
+ */
+ if(settings.get("sketch.id") == null){
+ // TODO: Should this watch the file or should it update a bunch on running the sketch?
+ settings.set("sketch.id", UUID.randomUUID().toString())
+ settings.save()
+ }
+ }
+ val stateVertical = rememberScrollState(0)
+
+ ProcessingTheme {
+ Box {
+ Row(
+ modifier = Modifier
+ .background(Color(Theme.getColor("editor.line.highlight.color").rgb))
+ .padding(start = Editor.LEFT_GUTTER.dp)
+ .fillMaxSize()
+ .verticalScroll(stateVertical)
+ .padding(vertical = 4.dp)
+ ) {
+ PluginsPanel(settings)
+ }
+ VerticalScrollbar(
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ .padding(8.dp)
+ .fillMaxHeight(),
+ adapter = rememberScrollbarAdapter(stateVertical)
+ )
+ }
+ }
+ }
+
+ @Composable
+ private fun PluginsPanel(settings: Settings) {
+ // Grab the installed plugins
+ val plugins = GradlePlugin.plugins
+
+ // Grab the enabled plugins
+ val pluginSetting = (settings.get(GradlePlugin.PROPERTIES_KEY) ?: "")
+ .split(",")
+ .map { it.trim() }
+ .filter{ it.isNotEmpty() }
+
+ // Link plugins in the settings to their installed counterparts
+ val enabledPlugins = pluginSetting
+ .map { id -> plugins.find { plugin -> plugin.id == id } }
+ Column {
+ Text(
+ text = text("gradle.settings.plugins"),
+ textAlign = TextAlign.Start,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ GradlePlugin.plugins.map { plugin ->
+ Row() {
+ Checkbox(
+ checked = enabledPlugins.contains(plugin),
+ modifier = Modifier
+ .padding(start = 0.dp, end = 8.dp)
+ .size(24.dp),
+ onCheckedChange = { checked ->
+ scope.launch {
+ // Work from the setting as we do not want to remove missing plugins
+ val current = pluginSetting.toMutableSet()
+ if (checked) {
+ current.add(plugin.id)
+ } else {
+ current.remove(plugin.id)
+ }
+ settings.set(GradlePlugin.PROPERTIES_KEY, current.joinToString(","))
+ settings.save()
+ }
+ },
+ )
+ Column {
+ Text(
+ text = plugin.name,
+ textAlign = TextAlign.Start,
+ fontSize = 12.sp
+ )
+ Text(
+ text = plugin.description,
+ textAlign = TextAlign.Start,
+ fontSize = 10.sp,
+ )
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/Log.kt b/app/src/processing/app/gradle/Log.kt
new file mode 100644
index 0000000000..30ca070cf6
--- /dev/null
+++ b/app/src/processing/app/gradle/Log.kt
@@ -0,0 +1,30 @@
+package processing.app.gradle
+
+import processing.app.Messages
+import java.io.PrintStream
+import java.net.ServerSocket
+
+class Log{
+ companion object{
+ fun startLogServer(port: Int, target: PrintStream){
+ val server = ServerSocket(port)
+ Messages.Companion.log("Log server started on port $port")
+ val client = server.accept()
+ Messages.Companion.log("Log server client connected")
+
+ val reader = client.getInputStream().bufferedReader()
+ try {
+ reader.forEachLine { line ->
+ if (line.isNotBlank()) {
+ target.println(line)
+ }
+ }
+ } catch (e: Exception) {
+ Messages.Companion.log("Error while reading from log server: ${e.message}")
+ } finally {
+ client.close()
+ server.close()
+ }
+ }
+ }
+}
diff --git a/app/src/processing/app/gradle/api/Sketch.kt b/app/src/processing/app/gradle/api/Sketch.kt
new file mode 100644
index 0000000000..635094ec83
--- /dev/null
+++ b/app/src/processing/app/gradle/api/Sketch.kt
@@ -0,0 +1,66 @@
+package processing.app.gradle.api
+
+import com.github.ajalt.clikt.command.SuspendingCliktCommand
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.parameters.options.default
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.required
+import processing.app.Base
+import processing.app.Platform
+import processing.app.Preferences
+import processing.app.contrib.ModeContribution
+import processing.app.gradle.GradleJob
+import processing.app.gradle.GradleService
+
+class Sketch : SuspendingCliktCommand("sketch") {
+ init {
+ subcommands(
+ Run()
+ )
+ }
+
+ override fun help(context: Context): String {
+ return """Manage sketches in the Processing environment."""
+ }
+
+ override suspend fun run() {
+ System.setProperty("java.awt.headless", "true")
+ }
+
+ class Run : SuspendingCliktCommand(name = "run") {
+ val sketch by option("--sketch", help = "The sketch to run")
+ .required()
+
+ val mode by option("--mode", help = "The mode to use for running the sketch (only java is supported for now)")
+
+ override fun help(context: Context): String {
+ return "Run the Processing sketch."
+ }
+
+ override suspend fun run() {
+ Base.setCommandLine()
+ Platform.init()
+ Preferences.init()
+ Base.locateSketchbookFolder()
+
+ // TODO: Support modes other than Java
+ val mode = ModeContribution.load(
+ null, Platform.getContentFile("modes/java"),
+ "processing.mode.java.JavaMode"
+ ).mode ?: throw IllegalStateException("Java mode not found")
+
+ System.setProperty("java.awt.headless", "false")
+
+ val service = GradleService(mode,null)
+ service.sketch.value = processing.app.Sketch(sketch, mode)
+ service.run()
+
+ // TODO: Use an async way to wait for the job to finish
+ //Wait for the service to finish
+ while (service.jobs.any { it.state.value != GradleJob.State.DONE }) {
+ Thread.sleep(100)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java
index df2440d391..73aa2ef40a 100644
--- a/app/src/processing/app/ui/Editor.java
+++ b/app/src/processing/app/ui/Editor.java
@@ -52,6 +52,7 @@
import processing.app.*;
import processing.utils.SketchException;
import processing.app.contrib.ContributionManager;
+import processing.app.gradle.GradleService;
import processing.app.laf.PdeMenuItemUI;
import processing.app.syntax.*;
import processing.core.*;
@@ -64,6 +65,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
protected Base base;
protected EditorState state;
protected Mode mode;
+ protected GradleService service;
// There may be certain gutter sizes that cause text bounds
// inside the console to be calculated incorrectly.
@@ -147,6 +149,7 @@ protected Editor(final Base base, String path, final EditorState state,
this.base = base;
this.state = state;
this.mode = mode;
+ this.service = new GradleService(this.mode,this);
// Make sure Base.getActiveEditor() never returns null
base.checkFirstEditor(this);
@@ -379,6 +382,9 @@ public EditorFooter createFooter() {
return ef;
}
+ public EditorFooter getFooter() {
+ return footer;
+ }
public void addErrorTable(EditorFooter ef) {
JScrollPane scrollPane = new JScrollPane();
@@ -468,6 +474,9 @@ public Mode getMode() {
return mode;
}
+ public GradleService getService() {
+ return service;
+ }
public void repaintHeader() {
header.repaint();
@@ -578,6 +587,7 @@ protected int getDividerLocation() {
* with things in the Preferences window.
*/
public void applyPreferences() {
+ service.setEnabled(Preferences.getBoolean("run.use_gradle"));
// Even though this is only updating the theme (colors, icons),
// subclasses use this to apply other preferences.
// For instance, Java Mode applies changes to error checking.
@@ -2287,6 +2297,7 @@ protected void handleOpenInternal(String path) throws EditorException {
} catch (IOException e) {
throw new EditorException("Could not create the sketch.", e);
}
+ service.setSketch(sketch);
header.rebuild();
updateTitle();
diff --git a/app/src/processing/app/ui/EditorConsole.java b/app/src/processing/app/ui/EditorConsole.java
index c8c40ee487..f49283823f 100644
--- a/app/src/processing/app/ui/EditorConsole.java
+++ b/app/src/processing/app/ui/EditorConsole.java
@@ -254,7 +254,14 @@ private boolean suppressMessage(String what, boolean err) {
// "java.lang.NoSuchMethodError: accessibilityHitTest"
// https://github.com/processing/processing4/issues/368
return true;
+ } else if (what.contains("__MOVE__")) {
+ // Don't display the "Move" message that is used to position the sketch window
+ return true;
+ }else if (what.startsWith("SLF4J: ")) {
+ // Don't display the SLF4J messages
+ return true;
}
+
} else { // !err
if (what.contains("Listening for transport dt_socket at address")) {
// Message from the JVM about the socket launch for debug
diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java
index bc09b2376a..945c5ffd04 100644
--- a/app/src/processing/app/ui/EditorFooter.java
+++ b/app/src/processing/app/ui/EditorFooter.java
@@ -153,13 +153,19 @@ public void addPanel(Component comp, String name) {
public void addPanel(Component comp, String name, String icon) {
tabs.add(new Tab(comp, name, icon));
cardPanel.add(name, comp);
+ repaint();
}
-
-// public void setPanel(int index) {
-// cardLayout.show(cardPanel, tabs.get(index).name);
-// }
-
+ /**
+ * Remove a panel from the footer.
+ * @param comp Component that links to this tab.
+ * */
+ public void removePanel(Component comp){
+ cardLayout.show(cardPanel, tabs.get(0).title);
+ tabs.removeIf(tab -> tab.comp == comp);
+ cardPanel.remove(comp);
+ repaint();
+ }
public void setPanel(Component comp) {
for (Tab tab : tabs) {
diff --git a/app/src/processing/app/ui/PreferencesFrame.java b/app/src/processing/app/ui/PreferencesFrame.java
index a8cf68c27d..28424a2ea3 100644
--- a/app/src/processing/app/ui/PreferencesFrame.java
+++ b/app/src/processing/app/ui/PreferencesFrame.java
@@ -85,6 +85,7 @@ public class PreferencesFrame {
JCheckBox hidpiDisableBox;
// JLabel hidpiRestartLabel;
JCheckBox syncSketchNameBox;
+ JCheckBox useModernBuildSystem;
JComboBox displaySelectionBox;
JComboBox languageSelectionBox;
@@ -554,6 +555,9 @@ public void mouseExited(MouseEvent e) {
runningPanel.setBorder(new TitledBorder("Running"));
runningPanel.setLayout(new BoxLayout(runningPanel, BoxLayout.Y_AXIS));
+ useModernBuildSystem = new JCheckBox(Language.text("preferences.use_modern_build_system"));
+ addRow(runningPanel, useModernBuildSystem);
+
addRow(runningPanel, displayLabel, displaySelectionBox);
addRow(runningPanel, backgroundColorLabel, presentColor);
addRow(runningPanel, memoryOverrideBox, memoryField, mbLabel);
@@ -827,6 +831,8 @@ protected void applyFrame() {
Preferences.setBoolean("pdex.completion", codeCompletionBox.isSelected());
Preferences.setBoolean("pdex.suggest.imports", importSuggestionsBox.isSelected());
+ Preferences.setBoolean("run.use_gradle", useModernBuildSystem.isSelected());
+
for (Editor editor : base.getEditors()) {
editor.applyPreferences();
}
@@ -902,6 +908,11 @@ public void showFrame() {
if (autoAssociateBox != null) {
autoAssociateBox.setSelected(Preferences.getBoolean("platform.auto_file_type_associations")); //$NON-NLS-1$
}
+
+ if(useModernBuildSystem != null) {
+ useModernBuildSystem.setSelected(Preferences.getBoolean("run.use_gradle"));
+ }
+
// The OK Button has to be set as the default button every time the
// PrefWindow is to be displayed
frame.getRootPane().setDefaultButton(okButton);
diff --git a/app/test/processing/app/gradle/GradleServiceTest.kt b/app/test/processing/app/gradle/GradleServiceTest.kt
new file mode 100644
index 0000000000..64a04d447c
--- /dev/null
+++ b/app/test/processing/app/gradle/GradleServiceTest.kt
@@ -0,0 +1,13 @@
+package processing.app.gradle
+
+import org.junit.jupiter.api.Assertions.*
+import processing.app.ui.Editor
+import kotlin.test.Test
+import org.mockito.kotlin.mock
+
+class GradleServiceTest{
+
+ @Test
+ fun testRunningSketch(){
+ }
+}
\ No newline at end of file
diff --git a/app/utils/build.gradle.kts b/app/utils/build.gradle.kts
index 193188f956..1618e1706b 100644
--- a/app/utils/build.gradle.kts
+++ b/app/utils/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
id("java")
+ alias(libs.plugins.mavenPublish)
}
repositories {
@@ -11,6 +12,15 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
}
+publishing{
+ repositories{
+ maven {
+ name = "App"
+ url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath)
+ }
+ }
+}
+
tasks.test {
useJUnitPlatform()
}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 8e7ad44a7a..dcd3080aca 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,6 +12,14 @@ plugins {
// Can be deleted after the migration to Gradle is complete
layout.buildDirectory = file(".build")
+allprojects{
+ tasks.withType {
+ options.encoding = "UTF-8"
+ }
+ tasks.withType {
+ options.encoding = "UTF-8"
+ }
+}
// Configure the dependencyUpdates task
tasks {
dependencyUpdates {
diff --git a/build/shared/lib/defaults.txt b/build/shared/lib/defaults.txt
index 1cfc190ca9..a91ccad1ed 100644
--- a/build/shared/lib/defaults.txt
+++ b/build/shared/lib/defaults.txt
@@ -190,6 +190,9 @@ console.temp.days = 7
console.scrollback.lines = 500
console.scrollback.chars = 40000
+# run java sketches with Gradle aka the Modern Build System
+run.use_gradle = false
+
# Any additional Java options when running.
# If you change this and can't run things, it's your own durn fault.
run.options =
diff --git a/build/shared/lib/footer/settings.svg b/build/shared/lib/footer/settings.svg
new file mode 100644
index 0000000000..25a9736fff
--- /dev/null
+++ b/build/shared/lib/footer/settings.svg
@@ -0,0 +1,8 @@
+
diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties
index 19a5c9f866..11d31c603a 100644
--- a/build/shared/lib/languages/PDE.properties
+++ b/build/shared/lib/languages/PDE.properties
@@ -238,6 +238,7 @@ preferences.launch_programs_in = Launch programs in
preferences.launch_programs_in.mode = mode
preferences.file = More preferences can be edited directly in the file:
preferences.file.hint = (Edit only when Processing is not running.)
+preferences.use_modern_build_system = Use modern build system (see Processing GitHub Wiki more details)
# Sketchbook Location (Frame)
sketchbook_location = Select new sketchbook folder
@@ -325,6 +326,13 @@ debugger.name = Name
debugger.value = Value
debugger.type = Type
+# Gradle
+gradle.instructions = About this file: \nProcessing creates this file when you run your sketch. \nIt configures the tools needed to build and export your code. \nLearn more: [Gradle Primer link]\n \nTo customize this file: \n1. Delete the line above that begins with '@processing-auto-generated'. \nThis will prevent Processing from overwriting this file in the future. \n2. Make your desired changes.
+gradle.using_gradle = Building sketch using the new build system. (See settings to switch to the legacy build system.)
+gradle.using_eclipse = Building sketch using the legacy build system. (See settings to switch to the new build system.)
+gradle.settings = Settings
+gradle.settings.plugins = Plugins (experimental)
+
# ---------------------------------------
# Toolbars
@@ -627,6 +635,24 @@ update_check = Update
update_check.updates_available.core = A new version of Processing is available,\nwould you like to visit the Processing download page?
update_check.updates_available.contributions = There are updates available for some of the installed contributions,\nwould you like to open the the Contribution Manager now?
+
+# ---------------------------------------
+# Welcome
+welcome.intro.title = Welcome to Processing
+welcome.intro.message = A flexible software sketchbook and a language for learning how to code.
+welcome.intro.suggestion = Is it your first time using Processing? Try one of the examples on the right.
+welcome.action.examples = More examples
+welcome.action.tutorials = Tutorials
+welcome.action.startup = Show this window at startup
+welcome.action.go = Let's go!
+
+# ---------------------------------------
+# Beta
+beta.window.title = Welcome to Beta
+beta.title = Welcome to the Processing Beta
+beta.message = Thank you for trying out the new version of Processing. We're very grateful!\n\nPlease report any bugs on the forums.
+beta.button = Got it!
+
# ---------------------------------------
# Beta
beta.window.title = Welcome to Beta
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 8f7211b131..f646bc9b8b 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -15,6 +15,7 @@ sourceSets{
main{
java{
srcDirs("src")
+ exclude("**/*.jnilib")
}
resources{
srcDirs("src")
@@ -34,10 +35,21 @@ dependencies {
testImplementation(libs.junit)
}
+publishing{
+ repositories{
+ maven {
+ name = "App"
+ url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath)
+ }
+ }
+}
mavenPublishing{
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
- signAllPublications()
+
+ // Only sign if signing is set up
+ if(project.hasProperty("signing.keyId") || project.hasProperty("signing.signingInMemoryKey"))
+ signAllPublications()
pom{
name.set("Processing Core")
@@ -77,3 +89,6 @@ tasks.withType {
tasks.compileJava{
options.encoding = "UTF-8"
}
+tasks.javadoc{
+ options.encoding = "UTF-8"
+}
diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java
index 4fccd1a535..d9df211eb7 100644
--- a/core/src/processing/core/PApplet.java
+++ b/core/src/processing/core/PApplet.java
@@ -705,7 +705,7 @@ public class PApplet implements PConstants {
protected boolean exitCalled;
// ok to be static because it's not possible to mix enabled/disabled
- static protected boolean disableAWT;
+ static protected boolean disableAWT = System.getProperty("processing.awt.disable", "false").equals("true");;
// messages to send if attached as an external vm
@@ -9940,19 +9940,21 @@ static public void runSketch(final String[] args,
System.exit(1);
}
- boolean external = false;
- int[] location = null;
- int[] editorLocation = null;
+ boolean external = System.getProperty("processing.external", "false").equals("true");;
+ int[] location = System.getProperty("processing.location", null) != null ?
+ parseInt(split(System.getProperty("processing.location"), ',')) : null;
+ int[] editorLocation = System.getProperty("processing.editor.location", null) != null ?
+ parseInt(split(System.getProperty("processing.editor.location"), ',')) : null;
String name = null;
int windowColor = 0;
int stopColor = 0xff808080;
- boolean hideStop = false;
+ boolean hideStop = System.getProperty("processing.stop.hide", "false").equals("true");
int displayNum = -1; // use default
- boolean present = false;
- boolean fullScreen = false;
- float uiScale = 0;
+ boolean present = System.getProperty("processing.present", "false").equals("true");
+ boolean fullScreen = System.getProperty("processing.fullscreen", "false").equals("true");
+ float uiScale = parseInt(System.getProperty("processing.uiScale", "0"), 0);
String param, value;
String folder = calcSketchPath();
diff --git a/java/README.md b/java/README.md
index a4be6e9a87..0a905208d0 100644
--- a/java/README.md
+++ b/java/README.md
@@ -3,8 +3,9 @@
This the Java Mode in Processing. It compiles your sketches and runs them. It is the primary mode of Processing.
## Folders
-- `application` assets for exporting applications within the mode
-- `generated` generated antlr code for the mode, should be moved to a proper `antlr` plugin within gradle
+- `application` assets for exporting applications within the mode (Deprecated)
+- `generated` generated antlr code for the mode, should be moved to a proper `antlr` plugin within gradle (Deprecated)
+- `gradle` the Processing java gradle plugin
- `libraries` libraries that are available within the mode
- `lsp` gradle build system for the language server protocol, in the future we should decouple the lsp from the java mode and pde and move all relevant code here. For now it can be found in `src/.../lsp`
- `mode` legacy files for `Ant`
@@ -13,8 +14,25 @@ This the Java Mode in Processing. It compiles your sketches and runs them. It is
- `test` tests for the mode
- `theme` assets for the mode, related to autocomplete and syntax highlighting
-## Future plans
-- Decouple the `lsp` and `preprocessor` from the mode and move them to their own repositories
-- Move the `antlr` code to a proper plugin within gradle
-- Create a gradle plugin to convert `.pde` file to `.java` files
-- Create a gradle based version of Java mode.
\ No newline at end of file
+## The Modern Build system
+
+Since 2025 work has started on creating a new internal build system for the Java Mode based on Gradle.
+The goal is to simplify by leaning more on Gradle, which provides a lot of the functionality that was build before out of the box and a lot more.
+
+### How it used to work
+
+The build system used to be based on some parts Ant, some parts eclipse (org.eclipse.jdt.core) and a lot of custom work build up over the years.
+
+### How it will work going forward
+
+The modern build system is based around Gradle, the main service (GradleService) for building a sketch with Gradle is included in `app` instead of into the Java mode as future modes are most likely also based on Gradle if they use `core` in some way. Most _Modes_ should/could probably be a Gradle plugin going forward.
+Breaking the build system away from the java mode will mean that we create an island of isolation when it comes to the build system, allowing contributors to work on the build system without running the editor.
+Another upside is that when we publish the Gradle plugin to the Gradle Plugin repository, it will become trivial to run Processing sketches outside the PDE and improvements made to the build system will be usable for everyone.
+There is now also an opportunity for creating contributions that modify the build system in more subtle ways rather than having to make a complete new mode, e.g. a compilation step for shaders or some setup tweaks to make JavaFX work out of the box.
+Furthermore, this change will embed Processing more into the wider Java ecosystem, if users want to upgrade from using Processing to Java whilst still using `core` that will become possible and won't need a rewrite of what they already created.
+
+### How to work on the modern build system
+
+If you want to work on the build system without the PDE, open `/java/gradle/example` into a new intellij IDEA window, this is set up to compile the Processing Java plugin and run sketches standalone.
+
+Within the editor, the gradle plugin is embedded in Processing's embedded maven repository so that Gradle can find it.
\ No newline at end of file
diff --git a/java/build.gradle.kts b/java/build.gradle.kts
index d314d44ed9..161f36769f 100644
--- a/java/build.gradle.kts
+++ b/java/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
+
plugins {
java
}
@@ -61,9 +63,8 @@ val bundle = tasks.register("extraResources"){
tasks.register("copyCore"){
val coreProject = project(":core")
dependsOn(coreProject.tasks.jar)
- from(coreProject.tasks.jar) {
- include("core*.jar")
- }
+ from(coreProject.tasks.jar)
+ include("core*.jar")
rename("core.+\\.jar", "core.jar")
into(coreProject.layout.projectDirectory.dir("library"))
}
diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts
new file mode 100644
index 0000000000..0171384f44
--- /dev/null
+++ b/java/gradle/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins{
+ `java-gradle-plugin`
+ alias(libs.plugins.gradlePublish)
+
+ kotlin("jvm") version libs.versions.kotlin
+}
+
+repositories {
+ mavenCentral()
+ maven("https://jogamp.org/deployment/maven")
+}
+
+dependencies{
+ implementation(project(":java:preprocessor"))
+
+ implementation(libs.composeGradlePlugin)
+ implementation(libs.kotlinGradlePlugin)
+ implementation(libs.kotlinComposePlugin)
+
+ testImplementation(project(":core"))
+ testImplementation(libs.junit)
+}
+
+// TODO: CI/CD for publishing the plugin to the Gradle Plugin Portal
+gradlePlugin{
+ plugins{
+ create("processing.java"){
+ id = "org.processing.java"
+ implementationClass = "org.processing.java.gradle.ProcessingPlugin"
+ }
+ }
+}
+publishing{
+ repositories{
+ mavenLocal()
+ maven {
+ name = "App"
+ url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath)
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/example/.idea/.gitignore b/java/gradle/example/.idea/.gitignore
new file mode 100644
index 0000000000..a0ccf77bc5
--- /dev/null
+++ b/java/gradle/example/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Environment-dependent path to Maven home directory
+/mavenHomeManager.xml
diff --git a/java/gradle/example/.idea/.name b/java/gradle/example/.idea/.name
new file mode 100644
index 0000000000..fb61c9d808
--- /dev/null
+++ b/java/gradle/example/.idea/.name
@@ -0,0 +1 @@
+processing-gradle-plugin-demo
\ No newline at end of file
diff --git a/java/gradle/example/.idea/compiler.xml b/java/gradle/example/.idea/compiler.xml
new file mode 100644
index 0000000000..b589d56e9f
--- /dev/null
+++ b/java/gradle/example/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/gradle/example/.idea/gradle.xml b/java/gradle/example/.idea/gradle.xml
new file mode 100644
index 0000000000..ae55d4d4a2
--- /dev/null
+++ b/java/gradle/example/.idea/gradle.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/gradle/example/.idea/kotlinc.xml b/java/gradle/example/.idea/kotlinc.xml
new file mode 100644
index 0000000000..d4b7accbaa
--- /dev/null
+++ b/java/gradle/example/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/gradle/example/.idea/misc.xml b/java/gradle/example/.idea/misc.xml
new file mode 100644
index 0000000000..5a50b6cd23
--- /dev/null
+++ b/java/gradle/example/.idea/misc.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/java/gradle/example/.idea/vcs.xml b/java/gradle/example/.idea/vcs.xml
new file mode 100644
index 0000000000..c2365ab11f
--- /dev/null
+++ b/java/gradle/example/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/gradle/example/brightness.pde b/java/gradle/example/brightness.pde
new file mode 100644
index 0000000000..1da8c36ac5
--- /dev/null
+++ b/java/gradle/example/brightness.pde
@@ -0,0 +1,32 @@
+/**
+ * Brightness
+ * by Rusty Robison.
+ *
+ * Brightness is the relative lightness or darkness of a color.
+ * Move the cursor vertically over each bar to alter its brightness.
+ */
+
+int barWidth = 20;
+int lastBar = -1;
+
+import controlP5.*;
+
+ControlP5 cp5;
+
+
+void setup() {
+ size(640, 360, P2D);
+ colorMode(HSB, width, 100, height);
+ noStroke();
+ background(0);
+}
+
+void draw() {
+ int whichBar = mouseX / barWidth;
+ if (whichBar != lastBar) {
+ int barX = whichBar * barWidth;
+ fill(barX, 100, mouseY);
+ rect(barX, 0, barWidth, height);
+ lastBar = whichBar;
+ }
+}
diff --git a/java/gradle/example/build.gradle.kts b/java/gradle/example/build.gradle.kts
new file mode 100644
index 0000000000..b476d51bba
--- /dev/null
+++ b/java/gradle/example/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins{
+ id("org.processing.java")
+}
\ No newline at end of file
diff --git a/java/gradle/example/settings.gradle.kts b/java/gradle/example/settings.gradle.kts
new file mode 100644
index 0000000000..ee9c97e155
--- /dev/null
+++ b/java/gradle/example/settings.gradle.kts
@@ -0,0 +1,5 @@
+rootProject.name = "processing-gradle-plugin-demo"
+
+pluginManagement {
+ includeBuild("../../../")
+}
\ No newline at end of file
diff --git a/java/gradle/hotreload/build.gradle.kts b/java/gradle/hotreload/build.gradle.kts
new file mode 100644
index 0000000000..e4873a8c31
--- /dev/null
+++ b/java/gradle/hotreload/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ `java-gradle-plugin`
+ kotlin("jvm") version libs.versions.kotlin
+ alias(libs.plugins.gradlePublish)
+
+}
+
+
+repositories {
+ mavenCentral()
+}
+
+dependencies{
+ implementation("org.jetbrains.compose.hot-reload:hot-reload-gradle-plugin:1.0.0-beta03")
+}
+
+gradlePlugin{
+ plugins{
+ create("processing.java.hotreload"){
+ id = "org.processing.java.hotreload"
+ implementationClass = "org.processing.java.gradle.ProcessingHotReloadPlugin"
+ }
+ }
+}
+publishing{
+ repositories{
+ mavenLocal()
+ maven {
+ name = "App"
+ url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath)
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt b/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt
new file mode 100644
index 0000000000..4776d4ebce
--- /dev/null
+++ b/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt
@@ -0,0 +1,29 @@
+package org.processing.java.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.jvm.toolchain.JvmVendorSpec
+import org.jetbrains.compose.reload.gradle.ComposeHotReloadPlugin
+
+class ProcessingHotReloadPlugin: Plugin {
+ override fun apply(project: Project) {
+ project.plugins.apply(ComposeHotReloadPlugin::class.java)
+
+ project.repositories.google()
+ project.extensions.getByType(JavaPluginExtension::class.java).toolchain {
+ it.languageVersion.set(JavaLanguageVersion.of(21))
+ it.vendor.set(JvmVendorSpec.JETBRAINS)
+ }
+
+ project.afterEvaluate {
+ project.tasks.named("build").configure { task ->
+ task.finalizedBy("reload")
+ }
+ project.tasks.named("run").configure { task ->
+ task.dependsOn("hotRun")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/DependenciesTask.kt b/java/gradle/src/main/kotlin/DependenciesTask.kt
new file mode 100644
index 0000000000..8e2cb9bca3
--- /dev/null
+++ b/java/gradle/src/main/kotlin/DependenciesTask.kt
@@ -0,0 +1,79 @@
+package org.processing.java.gradle
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.io.ObjectInputStream
+
+/*
+* The DependenciesTask resolves the dependencies for the sketch based on the libraries used
+ */
+abstract class DependenciesTask: DefaultTask() {
+ @InputFile
+ val librariesMetaData: RegularFileProperty = project.objects.fileProperty()
+
+ @InputFile
+ val sketchMetaData: RegularFileProperty = project.objects.fileProperty()
+
+ init{
+ librariesMetaData.convention(project.layout.buildDirectory.file("processing/libraries"))
+ sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch"))
+ }
+
+ @TaskAction
+ fun execute() {
+ val sketchMetaFile = sketchMetaData.get().asFile
+ val librariesMetaFile = librariesMetaData.get().asFile
+
+ val libraries = librariesMetaFile.inputStream().use { input ->
+ ObjectInputStream(input).readObject() as ArrayList
+ }
+
+ val sketch = sketchMetaFile.inputStream().use { input ->
+ ObjectInputStream(input).readObject() as PDETask.SketchMeta
+ }
+
+ val dependencies = mutableSetOf()
+
+ // Loop over the import statements in the sketch and import the relevant jars from the libraries
+ sketch.importStatements.forEach import@{ statement ->
+ libraries.forEach { library ->
+ library.jars.forEach { jar ->
+ jar.classes.forEach { className ->
+ if (className.startsWith(statement)) {
+ dependencies.addAll(library.jars.map { it.path } )
+ return@import
+ }
+ }
+ }
+ }
+ }
+ project.dependencies.add("implementation", project.files(dependencies) )
+
+ // TODO: Mutating the dependencies of configuration ':implementation' after it has been resolved or consumed. This
+
+ // TODO: Add only if user is compiling for P2D or P3D
+ // Add JOGL and Gluegen dependencies
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0")
+
+ val os = System.getProperty("os.name").lowercase()
+ val arch = System.getProperty("os.arch").lowercase()
+
+ val variant = when {
+ os.contains("mac") -> "macosx-universal"
+ os.contains("win") && arch.contains("64") -> "windows-amd64"
+ os.contains("linux") && arch.contains("aarch64") -> "linux-aarch64"
+ os.contains("linux") && arch.contains("arm") -> "linux-arm"
+ os.contains("linux") && arch.contains("amd64") -> "linux-amd64"
+ else -> throw GradleException("Unsupported OS/architecture: $os / $arch")
+ }
+
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-$variant")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-$variant")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:newt:2.5.0:natives-$variant")
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/LibrariesTask.kt b/java/gradle/src/main/kotlin/LibrariesTask.kt
new file mode 100644
index 0000000000..2ccca5cde7
--- /dev/null
+++ b/java/gradle/src/main/kotlin/LibrariesTask.kt
@@ -0,0 +1,81 @@
+package org.processing.java.gradle
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.io.ObjectOutputStream
+import java.util.jar.JarFile
+
+/*
+The libraries task scans the sketchbook libraries folder for all the libraries
+This task stores the resulting information in a file that can be used later to resolve dependencies
+ */
+abstract class LibrariesTask : DefaultTask() {
+
+ @InputFiles
+ val libraryDirectories: ConfigurableFileCollection = project.files()
+
+ @OutputFile
+ val librariesMetaData: RegularFileProperty = project.objects.fileProperty()
+
+ init{
+ librariesMetaData.convention { project.gradle.gradleUserHomeDir.resolve("common/processing/libraries") }
+ }
+
+ data class Jar(
+ val path: File,
+ val classes: List
+ ) : java.io.Serializable
+
+ data class Library(
+ val jars: List
+ ) : java.io.Serializable
+
+ @TaskAction
+ fun execute() {
+ val output = libraryDirectories.flatMap { librariesDirectory ->
+ if (!librariesDirectory.exists()) {
+ logger.error("Libraries directory (${librariesDirectory.path}) does not exist. Libraries will not be imported.")
+ return@flatMap emptyList()
+ }
+ val libraries = librariesDirectory
+ .listFiles { file -> file.isDirectory }
+ ?.map { folder ->
+ // Find all the jars in the sketchbook
+ val jars = folder.resolve("library")
+ .listFiles{ file -> file.extension == "jar" }
+ ?.map{ file ->
+
+ // Inside each jar, look for the defined classes
+ val jar = JarFile(file)
+ val classes = jar.entries().asSequence()
+ .filter { entry -> entry.name.endsWith(".class") }
+ .map { entry -> entry.name }
+ .map { it.substringBeforeLast('/').replace('/', '.') }
+ .distinct()
+ .toList()
+
+ // Return a reference to the jar and its classes
+ return@map Jar(
+ path = file,
+ classes = classes
+ )
+ }?: emptyList()
+
+ // Save the parsed jars and which folder
+ return@map Library(
+ jars = jars
+ )
+ }?: emptyList()
+
+ return@flatMap libraries
+ }
+ val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream())
+ meta.writeObject(output)
+ meta.close()
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/PDETask.kt b/java/gradle/src/main/kotlin/PDETask.kt
new file mode 100644
index 0000000000..f4bb6c78e9
--- /dev/null
+++ b/java/gradle/src/main/kotlin/PDETask.kt
@@ -0,0 +1,85 @@
+package org.processing.java.gradle
+
+import org.gradle.api.file.*
+import org.gradle.api.tasks.*
+import org.gradle.internal.file.Deleter
+import org.gradle.work.InputChanges
+import processing.mode.java.preproc.PdePreprocessor
+import java.io.File
+import java.io.ObjectOutputStream
+import java.io.Serializable
+import java.util.concurrent.Callable
+import java.util.jar.JarFile
+import javax.inject.Inject
+
+
+// TODO: Generate sourcemaps
+/*
+* The PDETask is the main task that processes the .pde files and generates the Java source code through the PdePreprocessor.
+ */
+abstract class PDETask : SourceTask() {
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:IgnoreEmptyDirectories
+ @get:SkipWhenEmpty
+ open val stableSources: FileCollection = project.files(Callable { this.source })
+
+ @OutputDirectory
+ val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
+
+ @get:Input
+ var sketchName: String = "processing"
+
+ @OutputFile
+ val sketchMetaData: RegularFileProperty = project.objects.fileProperty()
+
+ init{
+ outputDirectory.convention(project.layout.buildDirectory.dir("generated/pde"))
+ sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch"))
+ }
+
+ data class SketchMeta(
+ val sketchName: String,
+ val sketchRenderer: String?,
+ val importStatements: List,
+ val headerOffset: Int,
+ val edits: List
+ ) : Serializable
+
+ @TaskAction
+ fun execute() {
+ // Using stableSources since we can only run the pre-processor on the full set of sources
+ val combined = stableSources
+ .files
+ .groupBy { it.name }
+ .map { entry ->
+ entry.value.firstOrNull { it.parentFile?.name == "unsaved" }
+ ?: entry.value.first()
+ }
+ .joinToString("\n"){
+ it.readText()
+ }
+ val javaFile = File(outputDirectory.get().asFile, "$sketchName.java").bufferedWriter()
+
+ val meta = PdePreprocessor
+ .builderFor(sketchName)
+ .setTabSize(4)
+ .build()
+ .write(javaFile, combined)
+
+ javaFile.flush()
+ javaFile.close()
+
+ val sketchMeta = SketchMeta(
+ sketchName = sketchName,
+ sketchRenderer = meta.sketchRenderer,
+ importStatements = meta.importStatements.map { importStatement -> importStatement.packageName },
+ headerOffset = meta.headerOffset,
+ edits = meta.edits.map { it.toString() }
+ )
+
+ val metaFile = ObjectOutputStream(sketchMetaData.get().asFile.outputStream())
+ metaFile.writeObject(sketchMeta)
+ metaFile.close()
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
new file mode 100644
index 0000000000..df558710f3
--- /dev/null
+++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
@@ -0,0 +1,216 @@
+package org.processing.java.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.file.SourceDirectorySet
+import org.gradle.api.internal.file.DefaultSourceDirectorySet
+import org.gradle.api.internal.tasks.TaskDependencyFactory
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.plugins.JavaPlugin
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.tasks.JavaExec
+import org.jetbrains.compose.ComposeExtension
+import org.jetbrains.compose.desktop.DesktopExtension
+import java.io.File
+import java.net.Socket
+import javax.inject.Inject
+
+class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFactory) : Plugin {
+ override fun apply(project: Project) {
+ val sketchName = project.layout.projectDirectory.asFile.name.replace(Regex("[^a-zA-Z0-9_]"), "_")
+
+ val isProcessing = project.findProperty("processing.version") != null
+ val processingVersion = project.findProperty("processing.version") as String? ?: "4.3.4"
+ val processingGroup = project.findProperty("processing.group") as String? ?: "org.processing"
+ val workingDir = project.findProperty("processing.workingDir") as String?
+ val debugPort = project.findProperty("processing.debugPort") as String?
+ val logPort = project.findProperty("processing.logPort") as String?
+ val errPort = project.findProperty("processing.errPort") as String?
+
+ // TODO: Setup sketchbook when using as a standalone plugin, use the Java Preferences
+ val sketchbook = project.findProperty("processing.sketchbook") as String?
+ val settings = project.findProperty("processing.settings") as String?
+ val root = project.findProperty("processing.root") as String?
+
+ // Apply the Java plugin to the Project, equivalent of
+ // plugins {
+ // java
+ // }
+ project.plugins.apply(JavaPlugin::class.java)
+
+ if(isProcessing){
+ // Set the build directory to a temp file so it doesn't clutter up the sketch folder
+ // Only if the build directory doesn't exist, otherwise proceed as normal
+ if(!project.layout.buildDirectory.asFile.get().exists()) {
+ project.layout.buildDirectory.set(File(project.findProperty("processing.workingDir") as String))
+ }
+ // Disable the wrapper in the sketch to keep it cleaner
+ project.tasks.findByName("wrapper")?.enabled = false
+ }
+
+ // Add kotlin support, equivalent of
+ // plugins {
+ // kotlin("jvm") version "1.8.0"
+ // kotlin("plugin.compose") version "1.8.0"
+ // }
+ project.plugins.apply("org.jetbrains.kotlin.jvm")
+ // Add jetpack compose support
+ project.plugins.apply("org.jetbrains.kotlin.plugin.compose")
+ // Add the compose plugin to wrap the sketch in an executable
+ project.plugins.apply("org.jetbrains.compose")
+
+ // Add the Processing core library (within Processing from the internal maven repo and outside from the internet), equivalent of
+ // dependencies {
+ // implementation("org.processing:core:4.3.4")
+ // }
+ project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}")
+
+ // Add the jars in the code folder, equivalent of
+ // dependencies {
+ // implementation(fileTree("src") { include("**/code/*.jar") })
+ // }
+ project.dependencies.add("implementation", project.fileTree("src").apply { include("**/code/*.jar") })
+
+ // Add the repositories necessary for building the sketch, equivalent of
+ // repositories {
+ // maven("https://jogamp.org/deployment/maven")
+ // mavenCentral()
+ // mavenLocal()
+ // }
+ project.repositories.add(project.repositories.maven { it.setUrl("https://jogamp.org/deployment/maven") })
+ project.repositories.add(project.repositories.mavenCentral())
+ project.repositories.add(project.repositories.mavenLocal())
+
+ // Configure the compose Plugin, equivalent of
+ // compose {
+ // application {
+ // mainClass.set(sketchName)
+ // nativeDistributions {
+ // includeAllModules()
+ // }
+ // }
+ // }
+ project.extensions.configure(ComposeExtension::class.java) { extension ->
+ extension.extensions.getByType(DesktopExtension::class.java).application { application ->
+ // Set the class to be executed initially
+ application.mainClass = sketchName
+ application.nativeDistributions.includeAllModules = true
+ if(debugPort != null) {
+ application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
+ }
+ }
+ }
+
+ // TODO: Add support for customizing distributables
+ // TODO: Setup sensible defaults for the distributables
+
+ // Add convenience tasks for running, presenting, and exporting the sketch outside of Processing
+ if(!isProcessing) {
+ project.tasks.create("sketch").apply {
+ group = "processing"
+ description = "Runs the Processing sketch"
+ dependsOn("run")
+ }
+ project.tasks.create("present").apply {
+ group = "processing"
+ description = "Presents the Processing sketch"
+ doFirst {
+ project.tasks.withType(JavaExec::class.java).configureEach { task ->
+ task.systemProperty("processing.fullscreen", "true")
+ }
+ }
+ finalizedBy("run")
+ }
+ project.tasks.create("export").apply {
+ group = "processing"
+ description = "Creates a distributable version of the Processing sketch"
+
+ dependsOn("createDistributable")
+
+ }
+ }
+
+ project.afterEvaluate {
+ // Copy the result of create distributable to the project directory
+ project.tasks.named("createDistributable") { task ->
+ task.doLast {
+ project.copy {
+ it.from(project.tasks.named("createDistributable").get().outputs.files)
+ it.into(project.layout.projectDirectory)
+ }
+ }
+ }
+ }
+
+ // Move the processing variables into javaexec tasks so they can be used in the sketch as well
+ project.tasks.withType(JavaExec::class.java).configureEach { task ->
+ project.properties
+ .filterKeys { it.startsWith("processing") }
+ .forEach { (key, value) -> task.systemProperty(key, value) }
+
+ // Connect the stdio to the PDE if ports are specified
+ if(logPort != null) task.standardOutput = Socket("localhost", logPort.toInt()).outputStream
+ if(errPort != null) task.errorOutput = Socket("localhost", errPort.toInt()).outputStream
+
+ }
+
+ // For every Java Source Set (main, test, etc) add a PDE source set that includes .pde files
+ // and a task to process them before compilation
+ project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.first().let{ sourceSet ->
+ val pdeSourceSet = objectFactory.newInstance(
+ DefaultPDESourceDirectorySet::class.java,
+ objectFactory.sourceDirectorySet("${sourceSet.name}.pde", "${sourceSet.name} Processing Source")
+ )
+
+ // Configure the PDE source set to include all .pde files in the sketch folder except those in the build directory
+ pdeSourceSet.apply {
+ srcDir("./")
+ srcDir("$workingDir/unsaved")
+
+ filter.include("**/*.pde")
+ filter.exclude("${project.layout.buildDirectory.asFile.get().name}/**")
+ }
+ sourceSet.allSource.source(pdeSourceSet)
+
+ // Add top level java source files
+ sourceSet.java.srcDir(project.layout.projectDirectory).apply {
+ include("/*.java")
+ }
+
+ // Scan the libraries before compiling the sketches
+ val librariesTaskName = sourceSet.getTaskName("scanLibraries", "PDE")
+ val librariesScan = project.tasks.register(librariesTaskName, LibrariesTask::class.java) { task ->
+ task.description = "Scans the libraries in the sketchbook"
+ task.libraryDirectories.from(sketchbook?.let { File(it, "libraries") }, root?.let { File(it).resolve("modes/java/libraries") })
+ }
+
+ // Create a task to process the .pde files before compiling the java sources
+ val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE")
+ val pdeTask = project.tasks.register(pdeTaskName, PDETask::class.java) { task ->
+ task.description = "Processes the ${sourceSet.name} PDE"
+ task.source = pdeSourceSet
+ task.sketchName = sketchName
+
+ // Set the output of the pre-processor as the input for the java compiler
+ sourceSet.java.srcDir(task.outputDirectory)
+ }
+
+ val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE")
+ project.tasks.register(depsTaskName, DependenciesTask::class.java){ task ->
+ // Link the output of the libraries task to the dependencies task
+ task.librariesMetaData.set(librariesScan.get().librariesMetaData)
+ task.dependsOn(pdeTask, librariesScan)
+ }
+
+ // Make sure that the PDE tasks runs before the java compilation task
+ project.tasks.named(sourceSet.compileJavaTaskName) { task ->
+ task.dependsOn(pdeTaskName, depsTaskName)
+ }
+ }
+ }
+ abstract class DefaultPDESourceDirectorySet @Inject constructor(
+ sourceDirectorySet: SourceDirectorySet,
+ taskDependencyFactory: TaskDependencyFactory
+ ) : DefaultSourceDirectorySet(sourceDirectorySet, taskDependencyFactory), SourceDirectorySet
+}
+
diff --git a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt
new file mode 100644
index 0000000000..b65f629afe
--- /dev/null
+++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt
@@ -0,0 +1,328 @@
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.GradleRunner
+import org.gradle.testkit.runner.UnexpectedBuildFailure
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.io.File
+import java.lang.management.ManagementFactory
+import java.net.URLClassLoader
+
+class ProcessingPluginTest{
+ // TODO: Test on multiple platforms since there are meaningful differences between the platforms
+ data class TemporaryProcessingSketchResult(
+ val buildResult: BuildResult,
+ val sketchFolder: File,
+ val classLoader: ClassLoader
+ )
+
+ fun createTemporaryProcessingSketch(vararg arguments: String, configure: (sketchFolder: File) -> Unit): TemporaryProcessingSketchResult{
+ val directory = TemporaryFolder()
+ directory.create()
+ val sketchFolder = directory.newFolder("sketch")
+ directory.newFile("sketch/build.gradle.kts").writeText("""
+ plugins {
+ id("org.processing.java")
+ }
+ """.trimIndent())
+ directory.newFile("sketch/settings.gradle.kts")
+ configure(sketchFolder)
+
+ val buildResult = GradleRunner.create()
+ .withProjectDir(sketchFolder)
+ .withArguments(*arguments)
+ .withPluginClasspath()
+ .withDebug(true)
+ .build()
+
+ val classDir = sketchFolder.resolve("build/classes/java/main")
+ val classLoader = URLClassLoader(arrayOf(classDir.toURI().toURL()), this::class.java.classLoader)
+
+ return TemporaryProcessingSketchResult(
+ buildResult,
+ sketchFolder,
+ classLoader
+ )
+ }
+
+ data class TemporaryProcessingLibraryResult(
+ val buildResult: BuildResult,
+ val libraryFolder: File
+ )
+
+ fun createTemporaryProcessingLibrary(name: String): TemporaryProcessingLibraryResult{
+ val directory = TemporaryFolder()
+ directory.create()
+ val libraryFolder = directory.newFolder("libraries",name)
+ directory.newFile("libraries/$name/build.gradle.kts").writeText("""
+ plugins {
+ java
+ }
+ tasks.jar{
+ destinationDirectory.set(file("library"))
+ }
+ """.trimIndent())
+ val srcDirectory = directory.newFolder("libraries", name,"src", "main", "java")
+ directory.newFile("libraries/$name/src/main/java/Example.java").writeText("""
+ package testing.example;
+
+ public class Example {
+ public void exampleMethod() {
+ System.out.println("Hello from Example library");
+ }
+ }
+ """.trimIndent())
+ directory.newFile("libraries/$name/settings.gradle.kts")
+ directory.newFile("libraries/$name/library.properties").writeText("""
+ name=$name
+ author=Test Author
+ version=1.0.0
+ sentence=An example library
+ paragraph=This is a longer description of the example library.
+ category=Examples
+ url=http://example.com
+ """.trimIndent())
+
+ if(isDebuggerAttached()){
+ openFolderInFinder(libraryFolder)
+ }
+
+ val buildResult = GradleRunner.create()
+ .withProjectDir(libraryFolder)
+ .withArguments("jar")
+ .withPluginClasspath()
+ .withDebug(true)
+ .build()
+
+
+ return TemporaryProcessingLibraryResult(
+ buildResult,
+ libraryFolder
+ )
+ }
+
+ @Test
+ fun testSinglePDE(){
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ println("Hello World");
+ }
+ """.trimIndent())
+ }
+
+ val sketchClass = classLoader.loadClass("sketch")
+
+ assert(sketchClass != null) {
+ "Class sketch not found"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) {
+ "Method setup not found in class sketch"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) {
+ "Method draw not found in class sketch"
+ }
+ }
+
+ @Test
+ fun testMultiplePDE(){
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ otherFunction();
+ }
+ """.trimIndent())
+ sketchFolder.resolve("sketch2.pde").writeText("""
+ void otherFunction(){
+ println("Hi");
+ }
+ """.trimIndent())
+ }
+
+ val sketchClass = classLoader.loadClass("sketch")
+
+ assert(sketchClass != null) {
+ "Class sketch not found"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "otherFunction" } != null) {
+ "Method otherFunction not found in class sketch"
+ }
+
+ }
+
+ @Test
+ fun testJavaSourceFile(){
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ println("Hello World");
+ }
+ """.trimIndent())
+ sketchFolder.resolve("extra.java").writeText("""
+ class SketchJava {
+ public void javaMethod() {
+ System.out.println("Hello from Java");
+ }
+ }
+ """.trimIndent())
+ }
+ val sketchJavaClass = classLoader.loadClass("SketchJava")
+
+ assert(sketchJavaClass != null) {
+ "Class SketchJava not found"
+ }
+
+ assert(sketchJavaClass?.methods?.find { method -> method.name == "javaMethod" } != null) {
+ "Method javaMethod not found in class SketchJava"
+ }
+ }
+
+ @Test
+ fun testWithUnsavedSource(){
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ println("Hello World");
+ }
+ """.trimIndent())
+ sketchFolder.resolve("../unsaved").mkdirs()
+ sketchFolder.resolve("../unsaved/sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ println("Hello World");
+ }
+
+ void newMethod(){
+ println("This is an unsaved method");
+ }
+ """.trimIndent())
+ sketchFolder.resolve("gradle.properties").writeText(""")
+ processing.workingDir = ${sketchFolder.parentFile.absolutePath}
+ """.trimIndent())
+ }
+ val sketchClass = classLoader.loadClass("sketch")
+
+ assert(sketchClass != null) {
+ "Class sketch not found"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "newMethod" } != null) {
+ "Method otherFunction not found in class sketch"
+ }
+ }
+
+ @Test
+ fun testImportingLibrary(){
+ val libraryResult = createTemporaryProcessingLibrary("ExampleLibrary")
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build") { sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ import testing.example.*;
+
+ Example example;
+
+ void setup(){
+ size(100, 100);
+ example = new Example();
+ example.exampleMethod();
+ }
+
+ void draw(){
+ println("Hello World");
+ }
+ """.trimIndent())
+ sketchFolder.resolve("gradle.properties").writeText(""")
+ processing.sketchbook = ${libraryResult.libraryFolder.parentFile.parentFile.absolutePath}
+ """.trimIndent())
+ }
+
+ val sketchClass = classLoader.loadClass("sketch")
+
+ assert(sketchClass != null) {
+ "Class sketch not found"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) {
+ "Method setup not found in class sketch"
+ }
+
+ assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) {
+ "Method draw not found in class sketch"
+ }
+ }
+
+ @Test
+ fun testUseInternalLibraries(){
+
+ }
+
+ @Test
+ fun testUseCodeJar(){
+ // TODO: test if adding jars to the code folder works
+ }
+
+ @Test
+ fun testBuildFailureOnError(){
+ var failedSuccesfully = false
+ try{
+ val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build","--stacktrace"){ sketchFolder ->
+ sketchFolder.resolve("sketch.pde").writeText("""
+ void setup(){
+ size(100, 100);
+ }
+
+ void draw(){
+ // Intentionally cause an error by removing the semicolon
+ println("Hello World")
+ }
+ """.trimIndent())
+ }
+ }catch (exception: UnexpectedBuildFailure){
+ failedSuccesfully = true
+ }
+ assert(failedSuccesfully) {
+ "Build did not fail on error"
+ }
+ }
+
+ fun isDebuggerAttached(): Boolean {
+ val runtimeMxBean = ManagementFactory.getRuntimeMXBean()
+ val inputArguments = runtimeMxBean.inputArguments
+ return inputArguments.any {
+ it.contains("-agentlib:jdwp")
+ }
+ }
+ fun openFolderInFinder(folder: File) {
+ if (!folder.exists() || !folder.isDirectory) {
+ println("Invalid directory: ${folder.absolutePath}")
+ return
+ }
+
+ val process = ProcessBuilder("open", folder.absolutePath)
+ .inheritIO()
+ .start()
+ process.waitFor()
+ }
+}
+
+
diff --git a/java/preprocessor/build.gradle.kts b/java/preprocessor/build.gradle.kts
index 8e4300d311..e108b58a4a 100644
--- a/java/preprocessor/build.gradle.kts
+++ b/java/preprocessor/build.gradle.kts
@@ -28,7 +28,6 @@ afterEvaluate{
}
dependencies{
- implementation(project(":core"))
implementation(project(":app:utils"))
implementation(libs.antlr)
diff --git a/java/src/processing/mode/java/Compiler.java b/java/src/processing/mode/java/Compiler.java
index c7ec613b45..a2ac110455 100644
--- a/java/src/processing/mode/java/Compiler.java
+++ b/java/src/processing/mode/java/Compiler.java
@@ -52,6 +52,7 @@ public class Compiler {
* @throws SketchException Only if there's a problem. Only then.
*/
static public boolean compile(JavaBuild build) throws SketchException {
+ System.out.println(Language.text("gradle.using_eclipse"));
// This will be filled in if anyone gets angry
SketchException exception = null;
diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java
index 3fab2c8b17..abf505784c 100644
--- a/java/src/processing/mode/java/JavaEditor.java
+++ b/java/src/processing/mode/java/JavaEditor.java
@@ -29,7 +29,6 @@
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -63,6 +62,8 @@
import processing.mode.java.tweak.SketchParser;
import processing.mode.java.tweak.TweakClient;
+import static processing.app.gradle.GradleSettings.addGradleSettings;
+
public class JavaEditor extends Editor {
JavaMode jmode;
@@ -207,6 +208,7 @@ public void rebuild() {
public EditorFooter createFooter() {
EditorFooter footer = super.createFooter();
addErrorTable(footer);
+ addGradleSettings(footer, service);
return footer;
}
@@ -501,6 +503,10 @@ public String getCommentPrefix() {
* Handler for Sketch → Export Application
*/
public void handleExportApplication() {
+ if(service.getEnabled()){
+ service.export();
+ return;
+ }
if (handleExportCheckModified()) {
statusNotice(Language.text("export.notice.exporting"));
ExportPrompt ep = new ExportPrompt(this, () -> {
@@ -651,6 +657,14 @@ public void handleTweak() {
protected void handleLaunch(boolean present, boolean tweak) {
prepareRun();
toolbar.activateRun();
+
+ if(this.service.getEnabled()){
+ System.setProperty("processing.fullscreen", present ? "true" : "false");
+ System.setProperty("processing.tweak", tweak ? "true" : "false");
+ this.service.run();
+ return;
+ }
+
synchronized (runtimeLock) {
runtimeLaunchRequested = true;
}
@@ -679,6 +693,11 @@ protected void handleLaunch(boolean present, boolean tweak) {
* session or performs standard stop action if not currently debugging.
*/
public void handleStop() {
+ if(this.service.getEnabled()){
+ this.service.stop();
+ return;
+ }
+
if (debugger.isStarted()) {
debugger.stopDebug();
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8f8cb74c7f..2e4debf7b3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -5,6 +5,8 @@ include(
"app",
"java",
"java:preprocessor",
+ "java:gradle",
+ "java:gradle:hotreload",
"java:libraries:dxf",
"java:libraries:io",
"java:libraries:net",