diff --git a/.gitignore b/.gitignore
index 26b5b97563..7ba11c7238 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,7 +98,9 @@ bin-test
processing-examples
# Maven ignores
+/.kotlin/sessions
.gradle
+.build/
core/build/
build/publish/
app/build
@@ -108,10 +110,20 @@ java/build/
/java/libraries/svg/bin
/java/preprocessor/build
/java/lsp/build
+/java/gradle/build
/.kotlin/sessions
/core/examples/build
+/java/gradle/build
+/java/gradle/example/.processing
.build/
/app/windows/obj
/java/gradle/build
/java/gradle/example/.processing
+/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
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index e9be690395..2db2e88c86 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.kotlin/errors/errors-1743146568860.log b/.kotlin/errors/errors-1743146568860.log
new file mode 100644
index 0000000000..7cd151b6c7
--- /dev/null
+++ b/.kotlin/errors/errors-1743146568860.log
@@ -0,0 +1,4 @@
+kotlin version: 2.0.20
+error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
+ 1. Kotlin compile daemon is ready
+
diff --git a/app/ant/processing/app/gradle/GradleService.java b/app/ant/processing/app/gradle/GradleService.java
new file mode 100644
index 0000000000..0beca36abd
--- /dev/null
+++ b/app/ant/processing/app/gradle/GradleService.java
@@ -0,0 +1,15 @@
+package processing.app.gradle;
+
+import processing.app.ui.Editor;
+
+public class GradleService {
+ public GradleService(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() {}
+}
diff --git a/app/ant/processing/app/gradle/ui/Toolbar.java b/app/ant/processing/app/gradle/ui/Toolbar.java
new file mode 100644
index 0000000000..9706ae64e6
--- /dev/null
+++ b/app/ant/processing/app/gradle/ui/Toolbar.java
@@ -0,0 +1,13 @@
+package processing.app.gradle.ui;
+
+import processing.app.ui.Editor;
+import processing.app.ui.EditorToolbar;
+
+import javax.swing.*;
+
+public class Toolbar {
+ Toolbar(Editor editor){}
+ public static JComponent legacyWrapped(Editor editor, EditorToolbar toolbar){
+ return toolbar;
+ }
+}
diff --git a/app/src/processing/app/ui/Welcome.java b/app/ant/processing/app/ui/Welcome.java
similarity index 100%
rename from app/src/processing/app/ui/Welcome.java
rename to app/ant/processing/app/ui/Welcome.java
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 865296d135..f34f0c0f07 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,9 +1,10 @@
+import org.gradle.internal.jvm.Jvm
import org.gradle.kotlin.dsl.support.zipTo
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
+import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download
-import org.jetbrains.kotlin.fir.scopes.impl.overrides
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -49,17 +50,21 @@ compose.desktop {
application {
mainClass = "processing.app.ui.Start"
- 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")
+ modules("jdk.jdi", "java.compiler", "jdk.accessibility", "jdk.zipfs", "java.management.rmi")
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Processing"
@@ -97,6 +102,7 @@ compose.desktop {
dependencies {
implementation(project(":core"))
+ runtimeOnly(project(":java"))
implementation(libs.flatlaf)
@@ -109,6 +115,7 @@ dependencies {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+ implementation(compose.materialIconsExtended)
implementation(compose.desktop.currentOs)
@@ -121,6 +128,10 @@ dependencies {
testImplementation(libs.mockitoKotlin)
testImplementation(libs.junitJupiter)
testImplementation(libs.junitJupiterParams)
+
+ implementation(gradleApi())
+ @OptIn(ExperimentalComposeLibrary::class)
+ testImplementation(compose.uiTest)
}
tasks.test {
@@ -250,7 +261,6 @@ tasks.register("generateSnapConfiguration"){
- openjdk-17-jre
override-prime: |
snapcraftctl prime
- chmod -R +x opt/processing/lib/app/resources/jdk-*
rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts
""".trimIndent()
dir.file("../snapcraft.yaml").asFile.writeText(content)
@@ -266,7 +276,7 @@ tasks.register("packageSnap"){
commandLine("snapcraft")
}
tasks.register("zipDistributable"){
- dependsOn("createDistributable", "setExecutablePermissions")
+ dependsOn("createDistributable")
group = "compose desktop"
val distributable = tasks.named("createDistributable").get()
@@ -298,9 +308,15 @@ afterEvaluate{
}
dependsOn("packageSnap", "zipDistributable")
}
+ tasks.named("prepareAppResources").configure {
+ dependsOn(
+ ":core:publishAllPublicationsToAppRepository",
+ ":java:gradle:publishAllPublicationsToAppRepository",
+ ":java:preprocessor:publishAllPublicationsToAppRepository"
+ )
+ }
}
-
// 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
@@ -322,41 +338,6 @@ tasks.register("includeJavaMode") {
into(composeResources("modes/java/mode"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
-tasks.register("includeJdk") {
- val os = DefaultNativePlatform.getCurrentOperatingSystem()
- val arch = when (System.getProperty("os.arch")) {
- "amd64", "x86_64" -> "x64"
- else -> System.getProperty("os.arch")
- }
- val platform = when {
- os.isWindows -> "windows"
- os.isMacOsX -> "mac"
- else -> "linux"
- }
-
- val javaVersion = System.getProperty("java.version").split(".")[0]
- val imageType = "jdk"
-
- src("https://api.adoptium.net/v3/binary/latest/" +
- "$javaVersion/ga/" +
- "$platform/" +
- "$arch/" +
- "$imageType/" +
- "hotspot/normal/eclipse?project=jdk")
-
- val extension = if (os.isWindows) "zip" else "tar.gz"
- val jdk = layout.buildDirectory.file("tmp/jdk-$platform-$arch.$extension")
- dest(jdk)
- overwrite(false)
- doLast {
- copy {
- val archive = if (os.isWindows) { zipTree(jdk) } else { tarTree(jdk) }
- from(archive){ eachFile{ permissions{ unix("755") } } }
- into(composeResources(""))
- }
- }
- finalizedBy("prepareAppResources")
-}
tasks.register("includeSharedAssets"){
from("../build/shared/")
into(composeResources(""))
@@ -401,21 +382,26 @@ tasks.register("includeJavaModeResources") {
from(java.layout.buildDirectory.dir("resources-bundled"))
into(composeResources("../"))
}
-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")
+tasks.register("includeJdk") {
+ dependsOn("createDistributable")
+ doFirst {
+ val jdk = Jvm.current().javaHome.absolutePath
+ val target = layout.buildDirectory.dir("compose/binaries").get().asFileTree.matching { include("**/include.jdk") }
+ .files
+ .firstOrNull()
+ ?.parentFile
+ ?.resolve("jdk")
+ ?.absolutePath
+ ?: error("Could not find include.jdk")
+
+ val isWindows = System.getProperty("os.name").lowercase().contains("win")
+ val command = if (isWindows) {
+ listOf("xcopy", "/E", "/I", "/Q", jdk, target)
+ } else {
+ listOf("cp", "-a", jdk, target)
+ }
+ ProcessBuilder(command).inheritIO().start().waitFor()
}
- duplicatesStrategy = DuplicatesStrategy.INCLUDE
- into(dir)
}
tasks.register("signResources"){
onlyIf {
@@ -427,19 +413,15 @@ tasks.register("signResources"){
dependsOn(
"includeCore",
"includeJavaMode",
- "includeJdk",
"includeSharedAssets",
"includeProcessingExamples",
"includeProcessingWebsiteExamples",
"includeJavaModeResources",
- "renameWindres"
)
finalizedBy("prepareAppResources")
val resourcesPath = composeResources("")
-
-
// find jars in the resources directory
val jars = mutableListOf()
doFirst{
@@ -505,9 +487,8 @@ tasks.register("signResources"){
}
file(composeResources("Info.plist")).delete()
}
-
-
}
+
afterEvaluate {
tasks.named("prepareAppResources").configure {
dependsOn(
@@ -516,31 +497,11 @@ afterEvaluate {
"includeSharedAssets",
"includeProcessingExamples",
"includeProcessingWebsiteExamples",
- "includeJavaModeResources",
- "renameWindres"
+ "includeJavaModeResources"
)
}
- tasks.register("setExecutablePermissions") {
- description = "Sets executable permissions on binaries in Processing.app resources"
- group = "compose desktop"
-
- doLast {
- val resourcesPath = layout.buildDirectory.dir("compose/binaries")
- fileTree(resourcesPath) {
- include("**/resources/**/bin/**")
- include("**/resources/**/*.sh")
- include("**/resources/**/*.dylib")
- include("**/resources/**/*.so")
- include("**/resources/**/*.exe")
- }.forEach { file ->
- if (file.isFile) {
- file.setExecutable(true, false)
- }
- }
- }
- }
tasks.named("createDistributable").configure {
- dependsOn("signResources", "includeJdk")
- finalizedBy("setExecutablePermissions")
+ dependsOn("signResources")
+ finalizedBy("includeJdk")
}
}
diff --git a/app/src/main/resources/default.png b/app/src/main/resources/default.png
new file mode 100644
index 0000000000..df13f36105
Binary files /dev/null and b/app/src/main/resources/default.png differ
diff --git a/app/src/main/resources/toolbar/More.svg b/app/src/main/resources/toolbar/More.svg
new file mode 100644
index 0000000000..c730781888
--- /dev/null
+++ b/app/src/main/resources/toolbar/More.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/src/main/resources/toolbar/Play.svg b/app/src/main/resources/toolbar/Play.svg
new file mode 100644
index 0000000000..3c82108d73
--- /dev/null
+++ b/app/src/main/resources/toolbar/Play.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/src/main/resources/toolbar/Screenshot.svg b/app/src/main/resources/toolbar/Screenshot.svg
new file mode 100644
index 0000000000..23033589e2
--- /dev/null
+++ b/app/src/main/resources/toolbar/Screenshot.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/src/main/resources/toolbar/Settings.svg b/app/src/main/resources/toolbar/Settings.svg
new file mode 100644
index 0000000000..8a634d8658
--- /dev/null
+++ b/app/src/main/resources/toolbar/Settings.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/src/main/resources/toolbar/Sketch Settings.svg b/app/src/main/resources/toolbar/Sketch Settings.svg
new file mode 100644
index 0000000000..a369ca0a75
--- /dev/null
+++ b/app/src/main/resources/toolbar/Sketch Settings.svg
@@ -0,0 +1,8 @@
+
diff --git a/app/src/main/resources/toolbar/Stop.svg b/app/src/main/resources/toolbar/Stop.svg
new file mode 100644
index 0000000000..d186155dc2
--- /dev/null
+++ b/app/src/main/resources/toolbar/Stop.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/src/main/resources/welcome/intro/bubble.svg b/app/src/main/resources/welcome/intro/bubble.svg
new file mode 100644
index 0000000000..a3997b1e79
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/bubble.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/app/src/main/resources/welcome/intro/long.svg b/app/src/main/resources/welcome/intro/long.svg
new file mode 100644
index 0000000000..004418ce1f
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/long.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/src/main/resources/welcome/intro/short.svg b/app/src/main/resources/welcome/intro/short.svg
new file mode 100644
index 0000000000..d08759c01c
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/short.svg
@@ -0,0 +1,17 @@
+
diff --git a/app/src/main/resources/welcome/intro/wavy.svg b/app/src/main/resources/welcome/intro/wavy.svg
new file mode 100644
index 0000000000..b244066fa1
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/wavy.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/src/processing/app/Language.java b/app/src/processing/app/Language.java
index d55c8b710c..bcc4385a53 100644
--- a/app/src/processing/app/Language.java
+++ b/app/src/processing/app/Language.java
@@ -183,6 +183,12 @@ static public Language init() {
return instance;
}
+ static public void reload(){
+ if(instance == null) return;
+ synchronized (Language.class) {
+ instance = new Language();
+ }
+ }
static private String get(String key) {
LanguageBundle bundle = init().bundle;
diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java
index 2af96bfd12..89b13c4354 100644
--- a/app/src/processing/app/Platform.java
+++ b/app/src/processing/app/Platform.java
@@ -391,17 +391,14 @@ static public File getContentFile(String name) {
static public File getJavaHome() {
var resourcesDir = System.getProperty("compose.application.resources.dir");
if(resourcesDir != null) {
- var jdkFolder = Arrays.stream(new File(resourcesDir).listFiles((dir, name) -> dir.isDirectory() && name.startsWith("jdk-")))
- .findFirst()
- .orElse(null);
- if(Platform.isMacOS()){
- return new File(jdkFolder, "Contents/Home");
+ var jdkFolder = new File(resourcesDir,"jdk");
+ if(jdkFolder.exists()){
+ return jdkFolder;
}
- return jdkFolder;
}
var home = System.getProperty("java.home");
- if(home != null && new File(home, "bin/java").exists()){
+ if(home != null){
return new File(home);
}
if (Platform.isMacOS()) {
diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt
index c5645c9bbc..4d854aaa6a 100644
--- a/app/src/processing/app/Preferences.kt
+++ b/app/src/processing/app/Preferences.kt
@@ -2,9 +2,13 @@ package processing.app
import androidx.compose.runtime.*
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
+import java.io.OutputStream
import java.nio.file.*
import java.util.Properties
@@ -12,28 +16,68 @@ import java.util.Properties
const val PREFERENCES_FILE_NAME = "preferences.txt"
const val DEFAULTS_FILE_NAME = "defaults.txt"
-fun PlatformStart(){
- Platform.inst ?: Platform.init()
-}
+class ReactiveProperties: Properties() {
+ val _stateMap = mutableStateMapOf()
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ _stateMap[key] = value
+ }
+ override fun getProperty(key: String): String? {
+ return _stateMap[key] ?: super.getProperty(key)
+ }
+
+ operator fun get(key: String): String? = getProperty(key)
+
+ operator fun set(key: String, value: String) {
+ setProperty(key, value)
+ }
+}
+val LocalPreferences = compositionLocalOf { error("No preferences provided") }
+@OptIn(FlowPreview::class)
@Composable
-fun loadPreferences(): Properties{
- PlatformStart()
+fun PreferencesProvider(content: @Composable () -> Unit){
+ remember {
+ Platform.init()
+ }
val settingsFolder = Platform.getSettingsFolder()
val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME)
-
if(!preferencesFile.exists()){
+ preferencesFile.mkdirs()
preferencesFile.createNewFile()
}
- watchFile(preferencesFile)
- return Properties().apply {
- load(ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: InputStream.nullInputStream())
- load(preferencesFile.inputStream())
+ val update = watchFile(preferencesFile)
+ val properties = remember(preferencesFile, update) { ReactiveProperties().apply {
+ load((ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME)?: InputStream.nullInputStream()).reader(Charsets.UTF_8))
+ load(preferencesFile.inputStream().reader(Charsets.UTF_8))
+ }}
+
+ val initialState = remember(properties) { properties._stateMap.toMap() }
+
+ LaunchedEffect(properties) {
+ snapshotFlow { properties._stateMap.toMap() }
+ .dropWhile { it == initialState }
+ .debounce(100)
+ .collect {
+ preferencesFile.outputStream().use { output ->
+ output.write(
+ properties.entries
+ .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.key.toString() })
+ .joinToString("\n") { (key, value) -> "$key=$value" }
+ .toByteArray()
+ )
+ }
+ }
+ }
+
+ CompositionLocalProvider(LocalPreferences provides properties){
+ content()
}
-}
+}
@Composable
fun watchFile(file: File): Any? {
val scope = rememberCoroutineScope()
@@ -53,6 +97,7 @@ fun watchFile(file: File): Any? {
if (modified.context() != path.fileName) continue
event = modified
}
+ Thread.sleep(100)
}
}
onDispose {
@@ -62,12 +107,4 @@ fun watchFile(file: File): Any? {
}
}
return event
-}
-val LocalPreferences = compositionLocalOf { error("No preferences provided") }
-@Composable
-fun PreferencesProvider(content: @Composable () -> Unit){
- val preferences = loadPreferences()
- CompositionLocalProvider(LocalPreferences provides preferences){
- content()
- }
}
\ No newline at end of file
diff --git a/app/src/processing/app/contrib/ui/ContributionManager.kt b/app/src/processing/app/contrib/ui/ContributionManager.kt
index 2ad472159b..4d21227a4d 100644
--- a/app/src/processing/app/contrib/ui/ContributionManager.kt
+++ b/app/src/processing/app/contrib/ui/ContributionManager.kt
@@ -22,8 +22,9 @@ import androidx.compose.ui.window.application
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import kotlinx.serialization.Serializable
+import processing.app.LocalPreferences
import processing.app.Platform
-import processing.app.loadPreferences
+import processing.app.ReactiveProperties
import java.net.URL
import java.util.*
import javax.swing.JFrame
@@ -106,7 +107,7 @@ fun contributionsManager(){
var localContributions by remember { mutableStateOf(listOf()) }
var error by remember { mutableStateOf(null) }
- val preferences = loadPreferences()
+ val preferences = LocalPreferences.current
LaunchedEffect(preferences){
try {
@@ -284,9 +285,9 @@ fun contributionsManager(){
}
-fun loadContributionProperties(preferences: Properties): List>{
+fun loadContributionProperties(preferences: ReactiveProperties): List>{
val result = mutableListOf>()
- val sketchBook = Path(preferences.getProperty("sketchbook.path.four", Platform.getDefaultSketchbookFolder().path))
+ val sketchBook = Path(preferences.getProperty("sketchbook.path.four") ?: Platform.getDefaultSketchbookFolder().path)
sketchBook.forEachDirectoryEntry{ contributionsFolder ->
if(!contributionsFolder.isDirectory()) return@forEachDirectoryEntry
val typeName = contributionsFolder.fileName.toString()
diff --git a/app/src/processing/app/gradle/Debugger.kt b/app/src/processing/app/gradle/Debugger.kt
new file mode 100644
index 0000000000..aea18ec133
--- /dev/null
+++ b/app/src/processing/app/gradle/Debugger.kt
@@ -0,0 +1,43 @@
+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
+ 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..01ebbcd2d2
--- /dev/null
+++ b/app/src/processing/app/gradle/Exceptions.kt
@@ -0,0 +1,99 @@
+package processing.app.gradle
+
+import com.sun.jdi.ObjectReference
+import com.sun.jdi.StackFrame
+import com.sun.jdi.StringReference
+import com.sun.jdi.VirtualMachine
+import com.sun.jdi.event.ExceptionEvent
+import com.sun.jdi.request.EventRequest
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import processing.app.Messages
+
+// TODO: Consider adding a panel to the footer
+class Exceptions {
+ companion object {
+ suspend fun listen(vm: VirtualMachine) {
+ 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()
+ val stackFrames = thread.frames()
+
+ println("\nπ¨ Exception Caught π¨")
+ println("Type : ${exception.referenceType().name()}")
+// println("Message : ${getExceptionMessage(exception)}")
+ println("Thread : ${thread.name()}")
+ println("Location : ${location.sourcePath()}:${location.lineNumber()}\n")
+
+ // Separate stack frames
+ val userFrames = mutableListOf()
+ val processingFrames = mutableListOf()
+
+ stackFrames.forEach { frame ->
+ val className = frame.location().declaringType().name()
+ if (className.startsWith("processing.")) {
+ processingFrames.add(frame)
+ } else {
+ userFrames.add(frame)
+ }
+ }
+
+ // Print user frames first
+ println("π Stacktrace (Your Code First):")
+ userFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
+
+ // Print Processing frames second
+ if (processingFrames.isNotEmpty()) {
+ println("\nπ§ Processing Stacktrace (Hidden Initially):")
+ processingFrames.forEachIndexed { index, frame -> printStackFrame(index, frame) }
+ }
+
+ println("ββββββββββββββββββββββββββββββββββ\n")
+ }
+
+ fun printStackFrame(index: Int, frame: StackFrame) {
+ val location = frame.location()
+ val method = location.method()
+ println(
+ " #$index ${location.sourcePath()}:${location.lineNumber()} -> ${
+ method.declaringType().name()
+ }.${method.name()}()"
+ )
+ }
+
+ // Extracts the exception's message
+ fun getExceptionMessage(exception: ObjectReference): String {
+ val messageMethod = exception.referenceType().methodsByName("getMessage").firstOrNull() ?: return "Unknown"
+ val messageValue =
+ exception.invokeMethod(null, messageMethod, emptyList(), ObjectReference.INVOKE_SINGLE_THREADED)
+ return (messageValue as? StringReference)?.value() ?: "Unknown"
+ }
+ }
+}
\ 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..32a25776dd
--- /dev/null
+++ b/app/src/processing/app/gradle/GradleJob.kt
@@ -0,0 +1,173 @@
+package processing.app.gradle
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import com.sun.jdi.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+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.DefaultFileLocation
+import org.gradle.tooling.events.problems.internal.DefaultSingleProblemEvent
+import org.gradle.tooling.events.task.TaskFinishEvent
+import org.gradle.tooling.events.task.TaskStartEvent
+import processing.app.Base
+import processing.app.Messages
+import java.io.InputStreamReader
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+
+// TODO: Move the error reporting to its own file
+// TODO: Move the output filtering to its own file
+abstract class GradleJob{
+ enum class State{
+ NONE,
+ BUILDING,
+ RUNNING,
+ DONE
+ }
+
+ var service: GradleService? = null
+ var configure: BuildLauncher.() -> Unit = {}
+
+ val state = mutableStateOf(State.NONE)
+ val vm = mutableStateOf(null)
+ val problems = mutableStateListOf()
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private val cancel = GradleConnector.newCancellationTokenSource()
+
+ private val outputStream = PipedOutputStream()
+ private val errorStream = PipedOutputStream()
+
+ fun start() {
+ service?.jobs?.add(this)
+ val connection = service?.connection ?: return
+ scope.launch {
+ try {
+ state.value = State.BUILDING
+
+ connection.newBuild()
+ .apply {
+ configure()
+ withCancellationToken(cancel.token())
+ addStateListener()
+ addDebugging()
+ setStandardOutput(outputStream)
+ setStandardError(errorStream)
+ run()
+ }
+ }catch (e: Exception){
+ Messages.log("Error while running: ${e.message}")
+ }finally {
+ state.value = State.DONE
+ vm.value = null
+ }
+ }
+ scope.launch {
+ try {
+ InputStreamReader(PipedInputStream(outputStream)).buffered().use { reader ->
+ reader.lineSequence()
+ .forEach { line ->
+ if (cancel.token().isCancellationRequested) {
+ return@launch
+ }
+ if (state.value != State.RUNNING) {
+ return@forEach
+ }
+ service?.editor?.console?.out?.println(line)
+ }
+ }
+ }catch (e: Exception){
+ Messages.log("Error while reading output: ${e.message}")
+ }
+ }
+ scope.launch {
+ try {
+ InputStreamReader(PipedInputStream(errorStream)).buffered().use { reader ->
+ reader.lineSequence()
+ .forEach { line ->
+ if (cancel.token().isCancellationRequested) {
+ return@launch
+ }
+ if (state.value != State.RUNNING) {
+ return@forEach
+ }
+ when{
+ line.contains("+[IMKClient subclass]: chose IMKClient_Modern") -> return@forEach
+ line.contains("+[IMKInputSession subclass]: chose IMKInputSession_Modern") -> return@forEach
+ line.startsWith("__MOVE__") -> return@forEach
+ else -> service?.editor?.console?.err?.println(line)
+ }
+ }
+ }
+ }catch (e: Exception){
+ Messages.log("Error while reading error: ${e.message}")
+ }
+ }
+
+ }
+
+ fun cancel(){
+ cancel.cancel()
+ }
+ private fun BuildLauncher.addStateListener(){
+ addProgressListener(ProgressListener { event ->
+ if(event is TaskStartEvent) {
+ when(event.descriptor.name) {
+ ":run" -> {
+ state.value = State.RUNNING
+ Messages.log("Start run")
+ }
+ }
+
+ }
+ if(event is TaskFinishEvent) {
+ when(event.descriptor.name){
+ ":jar"->{
+ state.value = State.NONE
+ Messages.log("Jar finished")
+ }
+ ":run"->{
+ state.value = State.NONE
+ }
+ }
+ }
+ if(event is DefaultSingleProblemEvent) {
+ // TODO: Move to UI instead of printing
+ if(event.definition.severity == Severity.ADVICE) return@ProgressListener
+ problems.add(event)
+
+ val path = (event.locations.firstOrNull() as DefaultFileLocation?)?.path
+
+ val header = """
+ ${event.definition.id.displayName}:
+ ${event.contextualLabel.contextualLabel}
+ """.trimIndent()
+
+ val details = event.details.details?.replace(path ?: "", "")
+ val solutions = event.solutions.joinToString("\n") { it.solution }
+ val content = "$header\n$details\n$solutions"
+ service?.editor?.console?.err?.println(content)
+ }
+ })
+ }
+
+ fun BuildLauncher.addDebugging() {
+ addProgressListener(ProgressListener { event ->
+ if (event !is TaskStartEvent) return@ProgressListener
+ if (event.descriptor.name != ":run") return@ProgressListener
+
+ scope.launch {
+ val debugger = Debugger.connect(service?.debugPort) ?: return@launch
+ vm.value = debugger
+ Exceptions.listen(debugger)
+ }
+
+ })
+ }
+}
\ 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..13085ee1df
--- /dev/null
+++ b/app/src/processing/app/gradle/GradleService.kt
@@ -0,0 +1,260 @@
+package processing.app.gradle
+
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.gradle.tooling.BuildLauncher
+import org.gradle.tooling.GradleConnector
+import org.gradle.tooling.ProjectConnection
+import processing.app.Base
+import processing.app.Language
+import processing.app.Messages
+import processing.app.Platform
+import processing.app.gradle.helpers.ActionGradleJob
+import processing.app.gradle.helpers.BackgroundGradleJob
+import processing.app.ui.Editor
+import java.io.*
+import javax.swing.SwingUtilities
+import javax.swing.event.DocumentEvent
+import javax.swing.event.DocumentListener
+import kotlin.io.path.writeText
+
+// TODO: Remove dependency on editor (editor is not mockable, or move editor away from JFrame)
+// TODO: Improve progress tracking
+// TODO: PoC new debugger/tweak mode
+// TODO: Allow for plugins to skip gradle entirely
+// TODO: Improve background building
+// TODO: Rename to Service?
+// TODO: Track build speed (for analytics?)
+
+// 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 editor: Editor) {
+ val folder: File get() = editor.sketch.folder
+ val active = mutableStateOf(true)
+
+ val jobs = mutableStateListOf()
+ val workingDir = kotlin.io.path.createTempDirectory()
+ val debugPort = (30_000..60_000).random()
+
+ var connection: ProjectConnection? = null
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ // Hooks for java to check if the Gradle service is running
+ fun getEnabled(): Boolean {
+ return active.value
+ }
+ fun setEnabled(active: Boolean) {
+ this.active.value = active
+ }
+
+ fun startService(){
+ Messages.log("Starting Gradle service at $folder")
+ // TODO: recreate connection if sketch folder changes
+ connection = GradleConnector.newConnector()
+ .forProjectDirectory(folder)
+ .connect()
+
+ startListening()
+ startBuilding()
+ }
+
+
+ private fun startBuilding(){
+ scope.launch {
+ // TODO: Improve the experience with unsaved
+ val job = BackgroundGradleJob()
+ job.service = this@GradleService
+ job.configure = {
+ setup()
+ forTasks("jar")
+ addArguments("--continuous")
+ }
+ job.start()
+ }
+ }
+
+ private fun startListening(){
+ SwingUtilities.invokeLater {
+ editor.sketch.code.forEach {
+ it.document.addDocumentListener(object : DocumentListener {
+ override fun insertUpdate(e: DocumentEvent) {
+ setupGradle()
+ }
+
+ override fun removeUpdate(e: DocumentEvent) {
+ setupGradle()
+ }
+
+ override fun changedUpdate(e: DocumentEvent) {
+ setupGradle()
+ }
+ })
+ }
+
+ // TODO: Attach listener to new tab created
+ }
+ // TODO: Stop all jobs on dispose
+ }
+ fun run(){
+ stopActions()
+ editor.console.clear()
+
+ val job = ActionGradleJob()
+ job.service = this
+ job.configure = {
+ setup()
+ forTasks("run")
+ }
+ job.start()
+ }
+
+ fun export(){
+ stopActions()
+ editor.console.clear()
+
+ val job = ActionGradleJob()
+ job.service = this
+ job.configure = {
+ setup()
+ forTasks("runDistributable")
+ }
+ job.start()
+ }
+
+ fun stop(){
+ stopActions()
+ startBuilding()
+ }
+
+ fun stopActions(){
+ jobs
+ .filterIsInstance()
+ .forEach(GradleJob::cancel)
+ }
+
+ private fun setupGradle(): MutableList {
+ // TODO: is this the best way to handle unsaved data?
+ // Certainly not...
+ // Gradle is not recognizing the unsaved files as changed
+ // Tricky as when we save the file the actual one will be the latest
+ val unsaved = editor.sketch.code
+ .filter { it.isModified }
+ .map { code ->
+ val file = workingDir.resolve("unsaved/${code.fileName}")
+ file.parent.toFile().mkdirs()
+ file.writeText(code.documentText)
+ code.fileName
+ }
+
+ val group = System.getProperty("processing.group", "org.processing")
+
+ val variables = mapOf(
+ "group" to group,
+ "version" to Base.getVersionName(),
+ "sketchFolder" to folder.absolutePath,
+ "workingDir" to workingDir.toAbsolutePath().toString(),
+ "settings" to Platform.getSettingsFolder().absolutePath.toString(),
+ "unsaved" to unsaved.joinToString(","),
+ "debugPort" to debugPort.toString(),
+ "present" to false, // TODO: Implement
+ "fullscreen" to false, // TODO: Implement
+ "display" to 1, // TODO: Implement
+ "external" to true,
+ "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
+ "sketch.folder" to folder.absolutePath,
+ //"location" to "0,0",
+ //"ui.scale" to "1.0",
+ )
+ val repository = Platform.getContentFile("repository").absolutePath.replace("""\""", """\\""")
+
+ val initGradle = workingDir.resolve("init.gradle.kts").apply {
+ val content = """
+ beforeSettings{
+ pluginManagement {
+ repositories {
+ maven { url = uri("$repository") }
+ gradlePluginPortal()
+ }
+ }
+ }
+ allprojects{
+ repositories {
+ maven { url = uri("$repository") }
+ mavenCentral()
+ }
+ }
+ """.trimIndent()
+
+ writeText(content)
+ }
+
+
+ val buildGradle = folder.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 != Base.getVersionName()) return@let true
+
+ val mode = contents.substringAfter("mode=").substringBefore(" ")
+ if(editor.mode.title != mode) return@let true
+
+ return@let Base.DEBUG
+ }
+ if (generate) {
+ Messages.log("build.gradle.kts not found or outdated in ${folder}, creating one")
+ val header = """
+ // @processing-auto-generated mode=${editor.mode.title} version=${Base.getVersionName()}
+ //
+ """.trimIndent()
+
+ val instructions = Language.text("gradle.instructions")
+ .split("\n")
+ .joinToString("\n") { "// $it" }
+
+ // TODO: Move the current configuration to java mode
+ // TODO: Allow for other plugins to be registered
+ // TODO: Allow for the whole configuration to be overridden
+ // TODO: Define new plugin / mode schema
+ val configuration = """
+ plugins{
+ id("org.processing.gradle") version "${Base.getVersionName()}"
+ }
+ """.trimIndent()
+ val content = "${header}\n${instructions}\n${configuration}"
+ buildGradle.writeText(content)
+ }
+ val settingsGradle = folder.resolve("settings.gradle.kts")
+ if (!settingsGradle.exists()) {
+ settingsGradle.createNewFile()
+ }
+
+ val arguments = mutableListOf("--init-script", initGradle.toAbsolutePath().toString())
+ if (!Base.DEBUG) arguments.add("--quiet")
+ arguments.addAll(variables.entries.map { "-Pprocessing.${it.key}=${it.value}" })
+
+ return arguments
+ }
+
+
+ private fun BuildLauncher.setup(extraArguments: List = listOf()) {
+ setJavaHome(Platform.getJavaHome())
+
+ val arguments = setupGradle()
+ arguments.addAll(extraArguments)
+ withArguments(*arguments.toTypedArray())
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/ScreenshotService.kt b/app/src/processing/app/gradle/ScreenshotService.kt
new file mode 100644
index 0000000000..5523e00f14
--- /dev/null
+++ b/app/src/processing/app/gradle/ScreenshotService.kt
@@ -0,0 +1,73 @@
+package processing.app.gradle
+
+import com.sun.jdi.ObjectReference
+import com.sun.jdi.VirtualMachine
+import com.sun.jdi.event.BreakpointEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.TimeSource
+
+// TODO: Move to java mode
+// TODO: Add more feedback when things go wrong
+// TODO: Check if the sketch has a draw method
+class ScreenshotService {
+ companion object{
+ fun takeScreenshot(vm: VirtualMachine, onComplete: (Path) -> Unit) {
+ val scope = CoroutineScope(Dispatchers.IO)
+ scope.launch {
+ val manager = vm.eventRequestManager()
+ val type = vm.classesByName("processing.core.PApplet").firstOrNull() ?: return@launch
+
+ val methodDraw = type.methodsByName("handleDraw").firstOrNull() ?: return@launch
+ val methodSave = type.methodsByName("save").firstOrNull() ?: return@launch
+
+ val tempFile = Files.createTempFile( "sketch", ".png")
+
+ val location = methodDraw.allLineLocations().last()
+
+ val breakpoint = manager.createBreakpointRequest(location)
+ breakpoint.enable()
+
+ val queue = vm.eventQueue()
+ val timeout = 5.seconds
+ val startTime = TimeSource.Monotonic.markNow()
+
+ while (startTime.elapsedNow() < timeout) {
+ try {
+ val events = queue.remove()
+ events.forEach { event ->
+ if (event !is BreakpointEvent) return@forEach
+
+ val thread = event.thread()
+ val frame = thread.frame(0)
+ val obj = frame.thisObject() ?: return@forEach
+
+ val arg = vm.mirrorOf(tempFile.toAbsolutePath().toString())
+
+ obj.invokeMethod(thread, methodSave, listOf(arg), ObjectReference.INVOKE_SINGLE_THREADED)
+
+ if (thread.isSuspended) {
+ thread.resume()
+ }
+ events.resume()
+ onComplete(tempFile)
+
+ return@launch
+ }
+ events.resume()
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ breakpoint.disable()
+ manager.deleteEventRequest(breakpoint)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/helpers/ActionGradleJob.kt b/app/src/processing/app/gradle/helpers/ActionGradleJob.kt
new file mode 100644
index 0000000000..1adc9e57b7
--- /dev/null
+++ b/app/src/processing/app/gradle/helpers/ActionGradleJob.kt
@@ -0,0 +1,5 @@
+package processing.app.gradle.helpers
+
+import processing.app.gradle.GradleJob
+
+class ActionGradleJob : GradleJob()
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/helpers/BackgroundGradleJob.kt b/app/src/processing/app/gradle/helpers/BackgroundGradleJob.kt
new file mode 100644
index 0000000000..dd1d32ef95
--- /dev/null
+++ b/app/src/processing/app/gradle/helpers/BackgroundGradleJob.kt
@@ -0,0 +1,5 @@
+package processing.app.gradle.helpers
+
+import processing.app.gradle.GradleJob
+
+class BackgroundGradleJob : GradleJob()
\ No newline at end of file
diff --git a/app/src/processing/app/gradle/ui/Toolbar.kt b/app/src/processing/app/gradle/ui/Toolbar.kt
new file mode 100644
index 0000000000..927b6240f1
--- /dev/null
+++ b/app/src/processing/app/gradle/ui/Toolbar.kt
@@ -0,0 +1,376 @@
+package processing.app.gradle.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.awt.ComposePanel
+import androidx.compose.ui.awt.SwingPanel
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.res.loadImageBitmap
+import androidx.compose.ui.res.loadSvgPainter
+import androidx.compose.ui.res.useResource
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowPosition
+import androidx.compose.ui.window.rememberWindowState
+import groovyjarjarantlr4.v4.runtime.misc.Args
+import processing.app.gradle.helpers.ActionGradleJob
+import processing.app.gradle.GradleJob
+import processing.app.gradle.ScreenshotService
+import processing.app.ui.Editor
+import processing.app.ui.EditorToolbar
+import processing.app.ui.Theme
+import processing.app.ui.theme.toColorInt
+import java.io.File
+import javax.swing.JComponent
+
+class Toolbar(val editor: Editor?) {
+ companion object {
+ @JvmStatic
+ fun legacyWrapped(editor: Editor, toolbar: EditorToolbar): JComponent {
+ val bar = Toolbar(editor)
+ val panel = ComposePanel().apply {
+ setContent {
+ val displayNew = editor.service.active.value
+ if (displayNew) {
+ bar.display()
+ return@setContent
+ }
+ SwingPanel(factory = {
+ toolbar
+ }, modifier = Modifier.fillMaxWidth().height(56.dp))
+ }
+ }
+
+ return panel
+ }
+ }
+
+ // TODO: Split into multiple files
+ // TODO: Make runnable outside of Processing IDE
+ @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+ @Composable
+ fun display() {
+ val startColor = Theme.get("toolbar.gradient.top").toColorInt()
+ val endColor = Theme.get("toolbar.gradient.bottom").toColorInt()
+ val colorStops = arrayOf(
+ 0.0f to Color(startColor),
+ 1f to Color(endColor)
+ )
+ Row(
+ modifier = Modifier.background(Brush.verticalGradient(colorStops = colorStops))
+ .fillMaxWidth()
+ .padding(start = Editor.LEFT_GUTTER.dp, end = Editor.RIGHT_GUTTER.dp)
+ .padding(vertical = 11.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ val windowState = rememberWindowState(
+ width = Dp.Unspecified,
+ height = Dp.Unspecified,
+ position = WindowPosition(alignment = Alignment.Center),
+ )
+
+ SketchButtons()
+
+ var screenshot by remember { mutableStateOf(null) }
+ screenshot?.apply {
+ Window(
+ onCloseRequest = {
+ screenshot = null
+ },
+ resizable = true,
+ title = "Screenshot",
+ state = windowState,
+ ) {
+ Column(modifier = Modifier.padding(16.dp).defaultMinSize(400.dp, 400.dp)) {
+ val bitmap = remember { loadImageBitmap(screenshot!!.inputStream()) }
+ screenshot?.let {
+ Image(
+ bitmap = bitmap,
+ contentDescription = "Screenshot",
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+
+ }
+ var showSketchSettings by remember { mutableStateOf(false) }
+ Row {
+ hoverPill(actions = {
+ actionButton(
+ onClick = {
+
+ }
+ ) {
+ val icon = useResource("toolbar/Settings.svg") { loadSvgPainter(it, Density(1f)) }
+ val color = LocalContentColor.current
+ Icon(
+ painter = icon,
+ contentDescription = "Settings",
+ tint = color
+ )
+ }
+ val vm = editor?.service?.jobs?.lastOrNull()?.vm?.value
+ actionButton(
+ enabled = vm != null,
+ onClick = {
+ vm ?: return@actionButton
+ ScreenshotService.takeScreenshot(vm) { file ->
+ screenshot = file.toFile()
+ }
+ }
+ ) {
+ val icon = useResource("toolbar/Screenshot.svg") { loadSvgPainter(it, Density(1f)) }
+ val color = LocalContentColor.current
+ Icon(
+ painter = icon,
+ contentDescription = "Screenshot",
+ tint = color
+ )
+ }
+
+
+
+ actionButton(
+ active = showSketchSettings,
+ modifier = Modifier
+ .onClick {
+ editor ?: return@onClick
+ val x = editor.location.x + editor.width
+ val y = editor.location.y
+ windowState.position = WindowPosition(
+ x = x.dp,
+ y = y.dp,
+ )
+ showSketchSettings = !showSketchSettings
+ }
+ ) {
+ val icon = useResource("toolbar/Sketch Settings.svg") { loadSvgPainter(it, Density(1f)) }
+ val color = LocalContentColor.current
+ Icon(
+ painter = icon,
+ contentDescription = "Sketch Settings",
+ tint = color
+ )
+ }
+
+ }, base = {
+ actionButton {
+ val icon = useResource("toolbar/More.svg") { loadSvgPainter(it, Density(1f)) }
+ val color = LocalContentColor.current
+ Icon(
+ painter = icon,
+ contentDescription = "More",
+ tint = color
+ )
+ }
+ })
+
+
+ Window(
+ visible = showSketchSettings,
+ onCloseRequest = {
+ showSketchSettings = false
+ },
+ resizable = true,
+ title = "Sketch Settings",
+ state = windowState,
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Chip(onClick = {
+ editor?.service?.active?.value = false
+ }) {
+ Text("Switch back to legacy")
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ fun SketchButtons() {
+ val job = editor?.service?.jobs?.filterIsInstance()?.lastOrNull()
+ val state = job?.state?.value ?: GradleJob.State.NONE
+ val isActive = state != GradleJob.State.NONE && state != GradleJob.State.DONE
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ actionButton(
+ active = isActive,
+ modifier = Modifier
+ .onPointerEvent(PointerEventType.Press) {
+ editor?.service?.run()
+ }
+ .padding(2.dp)
+ ) {
+ val color = LocalContentColor.current
+ Fading(visible = state == GradleJob.State.BUILDING, delayMillis = 2_500) {
+ // TODO: Add progress tracking
+ CircularProgressIndicator(
+ color = color,
+ strokeCap = StrokeCap.Round,
+ strokeWidth = 3.dp
+ )
+ }
+ val icon = useResource("toolbar/Play.svg") { loadSvgPainter(it, Density(1f)) }
+ Icon(
+ painter = icon,
+ contentDescription = "Play",
+ tint = color
+ )
+ }
+ Fading(visible = isActive) {
+ actionButton(
+ modifier = Modifier
+ .onPointerEvent(PointerEventType.Press) {
+ editor?.service?.stop()
+ }
+ ) {
+ val icon = useResource("toolbar/Stop.svg") { loadSvgPainter(it, Density(1f)) }
+ val color = LocalContentColor.current
+ Icon(
+ painter = icon,
+ contentDescription = "Stop",
+ tint = color
+ )
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ fun hoverPill(actions: @Composable () -> Unit, base: @Composable () -> Unit) {
+ var hover by remember { mutableStateOf(false) }
+ val baseColor = Theme.get("toolbar.button.enabled.field").toColorInt().let { Color(it) }
+
+ Row(
+ modifier = Modifier
+ .onPointerEvent(PointerEventType.Enter) {
+ hover = true
+ }
+ .onPointerEvent(PointerEventType.Exit) {
+ hover = false
+ }
+ .clip(CircleShape)
+ .background(baseColor)
+
+
+ ){
+ if(hover) actions()
+ base()
+ }
+ }
+
+
+ @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+ @Composable
+ fun actionButton(
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ active: Boolean = false,
+ onClick: () -> Unit = {},
+ content: @Composable () -> Unit
+ ) {
+ val baseColor = Theme.get("toolbar.button.enabled.field")
+ val baseTextColor = Theme.get("toolbar.button.enabled.glyph")
+
+ var hover by remember { mutableStateOf(false) }
+ val hoverColor = Theme.get("toolbar.button.rollover.field")
+ val hoverTextColor = Theme.get("toolbar.button.rollover.glyph")
+
+ var pressed by remember { mutableStateOf(false) }
+ val pressedColor = Theme.get("toolbar.button.pressed.field")
+ val pressedTextColor = Theme.get("toolbar.button.pressed.glyph")
+
+ val activeColor = Theme.get("toolbar.button.selected.field")
+ val activeTextColor = Theme.get("toolbar.button.pressed.glyph")
+
+ val color = when {
+ active -> activeColor
+ pressed -> pressedColor
+ hover -> hoverColor
+ else -> baseColor
+ }.toColorInt()
+
+ val textColor = when {
+ active -> activeTextColor
+ pressed -> pressedTextColor
+ hover -> hoverTextColor
+ else -> baseTextColor
+ }
+
+ Box(
+ modifier = Modifier
+ .onPointerEvent(PointerEventType.Enter) {
+ hover = true
+ }
+ .onPointerEvent(PointerEventType.Exit) {
+ hover = false
+ }
+ .onPointerEvent(PointerEventType.Press) {
+ pressed = true
+ }
+ .onPointerEvent(PointerEventType.Release) {
+ pressed = false
+ }
+ .height(34.dp)
+ .clip(CircleShape)
+ .aspectRatio(1f)
+ .background(color = Color(color))
+ .onClick{
+ if (enabled) {
+ onClick()
+ }
+ }
+ .then(modifier)
+ ) {
+ CompositionLocalProvider(LocalContentColor provides Color(textColor.toColorInt())) {
+ content()
+ }
+ }
+ }
+ @Composable
+ fun Fading(visible: Boolean, delayMillis: Int = 0, content: @Composable () -> Unit) {
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn(
+ animationSpec = tween(
+ delayMillis = delayMillis,
+ durationMillis = 250,
+ easing = LinearEasing
+ )
+ ),
+ exit = fadeOut(
+ animationSpec = tween(
+ durationMillis = 250,
+ easing = LinearEasing
+ )
+ )
+ ) {
+ content()
+ }
+ }
+}
\ 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 a06cbe2383..ce14433c48 100644
--- a/app/src/processing/app/ui/Editor.java
+++ b/app/src/processing/app/ui/Editor.java
@@ -48,7 +48,6 @@
import javax.swing.text.html.*;
import javax.swing.undo.*;
-import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.util.SystemInfo;
import processing.app.Base;
import processing.app.Formatter;
@@ -63,6 +62,8 @@
import processing.app.SketchCode;
import processing.app.SketchException;
import processing.app.contrib.ContributionManager;
+import processing.app.gradle.GradleService;
+import processing.app.gradle.ui.Toolbar;
import processing.app.laf.PdeMenuItemUI;
import processing.app.syntax.*;
import processing.core.*;
@@ -75,6 +76,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.
@@ -157,6 +159,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);
// Make sure Base.getActiveEditor() never returns null
base.checkFirstEditor(this);
@@ -220,7 +223,9 @@ public void windowDeactivated(WindowEvent e) {
rebuildModePopup();
toolbar = createToolbar();
- upper.add(toolbar);
+ // Wrapping the toolbar to be able to switch build systems dynamically
+ var wrapped = Toolbar.legacyWrapped(this, toolbar);
+ upper.add(wrapped);
header = createHeader();
upper.add(header);
@@ -389,6 +394,9 @@ public EditorFooter createFooter() {
return ef;
}
+ public EditorFooter getFooter() {
+ return footer;
+ }
public void addErrorTable(EditorFooter ef) {
JScrollPane scrollPane = new JScrollPane();
@@ -478,6 +486,9 @@ public Mode getMode() {
return mode;
}
+ public GradleService getService() {
+ return service;
+ }
public void repaintHeader() {
header.repaint();
@@ -491,6 +502,9 @@ public void rebuildHeader() {
public void rebuildModePopup() {
modePopup = new JMenu();
+
+
+
ButtonGroup modeGroup = new ButtonGroup();
for (final Mode m : base.getModeList()) {
JRadioButtonMenuItem item = new JRadioButtonMenuItem(m.getTitle());
@@ -514,6 +528,13 @@ public void rebuildModePopup() {
manageModes.addActionListener(e -> ContributionManager.openModes());
modePopup.add(manageModes);
+ JMenuItem enableModern = new JMenuItem(Language.text("toolbar.enable_modern"));
+ enableModern.addActionListener(e -> {
+ this.service.setEnabled(true);
+ });
+ modePopup.addSeparator();
+ modePopup.add(enableModern);
+
Toolkit.setMenuMnemsInside(modePopup);
}
@@ -2265,6 +2286,7 @@ protected void handleOpenInternal(String path) throws EditorException {
} catch (IOException e) {
throw new EditorException("Could not create the sketch.", e);
}
+ service.startService();
header.rebuild();
updateTitle();
diff --git a/app/src/processing/app/ui/Welcome.kt b/app/src/processing/app/ui/Welcome.kt
new file mode 100644
index 0000000000..492410b881
--- /dev/null
+++ b/app/src/processing/app/ui/Welcome.kt
@@ -0,0 +1,274 @@
+package processing.app.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import com.formdev.flatlaf.util.SystemInfo
+import processing.app.*
+import processing.app.ui.components.LanguageChip
+import processing.app.ui.components.examples.examples
+import processing.app.ui.theme.*
+import java.awt.Desktop
+import java.io.IOException
+import java.net.URI
+import java.nio.file.*
+import java.util.*
+import javax.swing.SwingUtilities
+
+
+class Welcome @Throws(IOException::class) constructor(base: Base) {
+ init {
+ SwingUtilities.invokeLater {
+ PDEWindow("menu.help.welcome", fullWindowContent = true) {
+ CompositionLocalProvider(LocalBase provides base) {
+ welcome()
+ }
+ }
+ }
+ }
+
+ companion object {
+ val LocalBase = compositionLocalOf { null }
+ @Composable
+ fun welcome() {
+ Column(
+ modifier = Modifier
+ .background(
+ Brush.linearGradient(
+ colorStops = arrayOf(0f to Color.Transparent, 1f to Color("#C0D7FF".toColorInt())),
+ start = Offset(815f, 0f),
+ end = Offset(815f * 2, 450f)
+ )
+ )
+ .padding(horizontal = 32.dp)
+ .padding(bottom = 32.dp)
+ .padding(top = if (SystemInfo.isMacFullWindowContentSupported) 22.dp else 0.dp)
+ .height(IntrinsicSize.Max)
+ .width(IntrinsicSize.Max)
+ ) {
+ Column(
+ horizontalAlignment = Alignment.End,
+ modifier = Modifier
+ .align(Alignment.End)
+ ) {
+ LanguageChip()
+ }
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(48.dp),
+ ) {
+ Column {
+ intro()
+ }
+ Box{
+ Column {
+ examples()
+ actions()
+ }
+ val locale = LocalLocale.current
+ Image(
+ painter = painterResource("welcome/intro/wavy.svg"),
+ contentDescription = locale["welcome.intro.long"],
+ modifier = Modifier
+ .height(200.dp)
+ .offset (32.dp)
+ .align(Alignment.BottomEnd)
+ .scale(when(LocalLayoutDirection.current) {
+ LayoutDirection.Rtl -> -1f
+ else -> 1f
+ }, 1f)
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun intro(){
+ val locale = LocalLocale.current
+ Column(
+ verticalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(IntrinsicSize.Max)
+ ) {
+ Column {
+ Text(
+ text = locale["welcome.intro.title"],
+ style = typography.h4,
+ modifier = Modifier
+ .sizeIn(maxWidth = 305.dp)
+ )
+ Text(
+ text = locale["welcome.intro.message"],
+ style = typography.body1,
+ modifier = Modifier
+ .sizeIn(maxWidth = 305.dp)
+ )
+ }
+ Column(
+ modifier = Modifier
+ .offset(y = 32.dp)
+ ){
+ Text(
+ text = locale["welcome.intro.suggestion"],
+ style = typography.body1,
+ color = colors.onPrimary,
+ modifier = Modifier
+ .padding(top = 16.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(colors.primary)
+ .padding(horizontal = 24.dp)
+ .padding(top = 16.dp, bottom = 24.dp)
+ .sizeIn(maxWidth = 200.dp)
+ )
+ Image(
+ painter = painterResource("welcome/intro/bubble.svg"),
+ contentDescription = locale["welcome.intro.long"],
+ modifier = Modifier
+ .align(Alignment.Start)
+ .scale(when(LocalLayoutDirection.current) {
+ LayoutDirection.Rtl -> -1f
+ else -> 1f
+ }, 1f)
+ .padding(start = 64.dp)
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.
+ fillMaxWidth()
+ ) {
+ Image(
+ painter = painterResource("welcome/intro/long.svg"),
+ contentDescription = locale["welcome.intro.long"],
+ modifier = Modifier
+ .offset(x = -32.dp)
+ .scale(when(LocalLayoutDirection.current) {
+ LayoutDirection.Rtl -> -1f
+ else -> 1f
+ }, 1f)
+ )
+ Image(
+ painter = painterResource("welcome/intro/short.svg"),
+ contentDescription = locale["welcome.intro.short"],
+ modifier = Modifier
+ .align(Alignment.Bottom)
+ .offset(x = 16.dp, y = -16.dp)
+ .scale(when(LocalLayoutDirection.current) {
+ LayoutDirection.Rtl -> -1f
+ else -> 1f
+ }, 1f)
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun actions(){
+ val locale = LocalLocale.current
+ val base = LocalBase.current
+ PDEChip(onClick = {
+ base?.defaultMode?.showExamplesFrame()
+ }) {
+ Text(
+ text = locale["welcome.action.examples"],
+ )
+ Image(
+ imageVector = Icons.AutoMirrored.Default.ArrowForward,
+ contentDescription = locale["welcome.action.tutorials"],
+ colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(typography.body1.fontSize.value.dp)
+ )
+ }
+ PDEChip(onClick = {
+ if (!Desktop.isDesktopSupported()) return@PDEChip
+ val desktop = Desktop.getDesktop()
+ if(!desktop.isSupported(Desktop.Action.BROWSE)) return@PDEChip
+ try {
+ desktop.browse(URI(System.getProperty("processing.tutorials")))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }) {
+ Text(
+ text = locale["welcome.action.tutorials"],
+ )
+ Image(
+ imageVector = Icons.AutoMirrored.Default.ArrowForward,
+ contentDescription = locale["welcome.action.tutorials"],
+ colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(typography.body1.fontSize.value.dp)
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .offset(-32.dp)
+ ) {
+ val preferences = LocalPreferences.current
+ Checkbox(
+ checked = preferences["welcome.four.show"]?.equals("true") ?: false,
+ onCheckedChange = {
+ preferences.setProperty("welcome.four.show", it.toString())
+ },
+ modifier = Modifier
+ .size(24.dp)
+ )
+ Text(
+ text = locale["welcome.action.startup"],
+ )
+ }
+ val window = LocalWindow.current
+ PDEButton(onClick = {
+ window.dispose()
+ }) {
+ Text(
+ text = locale["welcome.action.go"],
+ modifier = Modifier
+ )
+ }
+ }
+ }
+
+
+
+ @JvmStatic
+ fun main(args: Array) {
+ pdeapplication("menu.help.welcome", fullWindowContent = true) {
+ welcome()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt
index d7492fa6aa..6112820268 100644
--- a/app/src/processing/app/ui/WelcomeToBeta.kt
+++ b/app/src/processing/app/ui/WelcomeToBeta.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.formdev.flatlaf.util.SystemInfo
+import processing.app.ui.theme.*
import com.mikepenz.markdown.compose.Markdown
import com.mikepenz.markdown.m2.markdownColor
import com.mikepenz.markdown.m2.markdownTypography
@@ -54,44 +55,18 @@ import javax.swing.SwingUtilities
class WelcomeToBeta {
companion object{
val windowSize = Dimension(400, 200)
- val windowTitle = Locale()["beta.window.title"]
@JvmStatic
fun showWelcomeToBeta() {
- val mac = SystemInfo.isMacFullWindowContentSupported
SwingUtilities.invokeLater {
- JFrame(windowTitle).apply {
- val close = { dispose() }
- rootPane.putClientProperty("apple.awt.transparentTitleBar", mac)
- rootPane.putClientProperty("apple.awt.fullWindowContent", mac)
- defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
- contentPane.add(ComposePanel().apply {
- size = windowSize
- setContent {
- ProcessingTheme {
- Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) {
- welcomeToBeta(close)
- }
- }
- }
- })
- pack()
- background = java.awt.Color.white
- setLocationRelativeTo(null)
- addKeyListener(object : KeyAdapter() {
- override fun keyPressed(e: KeyEvent) {
- if (e.keyCode == KeyEvent.VK_ESCAPE) close()
- }
- })
- isResizable = false
- isVisible = true
- requestFocus()
+ PDEWindow("beta.window.title") {
+ welcomeToBeta()
}
}
}
@Composable
- fun welcomeToBeta(close: () -> Unit = {}) {
+ fun welcomeToBeta() {
Row(
modifier = Modifier
.padding(20.dp, 10.dp)
@@ -131,9 +106,10 @@ class WelcomeToBeta {
modifier = Modifier.background(Color.Transparent).padding(bottom = 10.dp)
)
Row {
+ val window = LocalWindow.current
Spacer(modifier = Modifier.weight(1f))
PDEButton(onClick = {
- close()
+ window.dispose()
}) {
Text(
text = locale["beta.button"],
@@ -144,66 +120,11 @@ class WelcomeToBeta {
}
}
}
- @OptIn(ExperimentalComposeUiApi::class)
- @Composable
- fun PDEButton(onClick: () -> Unit, content: @Composable BoxScope.() -> Unit) {
- val theme = LocalTheme.current
-
- var hover by remember { mutableStateOf(false) }
- var clicked by remember { mutableStateOf(false) }
- val offset by animateFloatAsState(if (hover) -5f else 5f)
- val color by animateColorAsState(if(clicked) colors.primaryVariant else colors.primary)
-
- Box(modifier = Modifier.padding(end = 5.dp, top = 5.dp)) {
- Box(
- modifier = Modifier
- .offset((-offset).dp, (offset).dp)
- .background(theme.getColor("toolbar.button.pressed.field"))
- .matchParentSize()
- )
- Box(
- modifier = Modifier
- .onPointerEvent(PointerEventType.Press) {
- clicked = true
- }
- .onPointerEvent(PointerEventType.Release) {
- clicked = false
- onClick()
- }
- .onPointerEvent(PointerEventType.Enter) {
- hover = true
- }
- .onPointerEvent(PointerEventType.Exit) {
- hover = false
- }
- .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR)))
- .background(color)
- .padding(10.dp)
- .sizeIn(minWidth = 100.dp),
- contentAlignment = Alignment.Center,
- content = content
- )
- }
- }
-
@JvmStatic
fun main(args: Array) {
- application {
- val windowState = rememberWindowState(
- size = DpSize.Unspecified,
- position = WindowPosition(Alignment.Center)
- )
-
- Window(onCloseRequest = ::exitApplication, state = windowState, title = windowTitle) {
- ProcessingTheme {
- Surface(color = colors.background) {
- welcomeToBeta {
- exitApplication()
- }
- }
- }
- }
+ pdeapplication("beta.window.title") {
+ welcomeToBeta()
}
}
}
diff --git a/app/src/processing/app/ui/components/LanuageSelector.kt b/app/src/processing/app/ui/components/LanuageSelector.kt
new file mode 100644
index 0000000000..5c42443fe4
--- /dev/null
+++ b/app/src/processing/app/ui/components/LanuageSelector.kt
@@ -0,0 +1,126 @@
+package processing.app.ui.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.outlined.Language
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.unit.dp
+import processing.app.Platform
+import processing.app.ui.theme.LocalLocale
+import processing.app.ui.theme.PDEChip
+import processing.app.watchFile
+import java.io.File
+import java.nio.file.FileSystem
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.*
+import kotlin.io.path.inputStream
+
+data class Language(
+ val name: String,
+ val code: String,
+ val locale: Locale,
+ val properties: Properties
+)
+
+var jarFs: FileSystem? = null
+
+@Composable
+fun LanguageChip(){
+ var expanded by remember { mutableStateOf(false) }
+
+ val settingsFolder = Platform.getSettingsFolder()
+ val languageFile = File(settingsFolder, "language.txt")
+ watchFile(languageFile)
+
+ val main = ClassLoader.getSystemResource("PDE.properties")?: return
+
+ val languages = remember {
+ val list = when(main.protocol){
+ "file" -> {
+ val path = Paths.get(main.toURI())
+ Files.list(path.parent)
+ }
+ "jar" -> {
+ val uri = main.toURI()
+ jarFs = jarFs ?: FileSystems.newFileSystem(uri, emptyMap()) ?: return@remember null
+ Files.list(jarFs!!.getPath("/"))
+ }
+ else -> null
+ } ?: return@remember null
+
+ list.toList()
+ .map { Pair(it, it.fileName.toString()) }
+ .filter { (_, fileName) -> fileName.startsWith("PDE_") && fileName.endsWith(".properties") }
+ .map { (path, _) ->
+ path.inputStream().reader(Charsets.UTF_8).use {
+ val properties = Properties()
+ properties.load(it)
+
+ val code = path.fileName.toString().removeSuffix(".properties").replace("PDE_", "")
+ val locale = Locale.forLanguageTag(code)
+ val name = locale.getDisplayName(locale)
+
+ return@map Language(
+ name,
+ code,
+ locale,
+ properties
+ )
+ }
+ }
+ .sortedBy { it.name.lowercase() }
+ } ?: return
+
+ val current = languageFile.readText(Charsets.UTF_8).substring(0, 2)
+ val currentLanguage = remember(current) { languages.find { it.code.startsWith(current) } ?: languages.first()}
+
+ val locale = LocalLocale.current
+
+ PDEChip(onClick = { expanded = !expanded }, leadingIcon = {
+ Image(
+ imageVector = Icons.Outlined.Language,
+ contentDescription = "Language",
+ colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(typography.body1.fontSize.value.dp)
+ )
+ }) {
+ Text(currentLanguage.name)
+ Image(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = locale["welcome.action.tutorials"],
+ colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+ modifier = Modifier
+ .size(typography.body1.fontSize.value.dp)
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {
+ expanded = false
+ },
+ ){
+ for (language in languages){
+ DropdownMenuItem(onClick = {
+ locale.set(language.locale)
+ expanded = false
+ }) {
+ Text(language.name)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/components/examples/Examples.kt b/app/src/processing/app/ui/components/examples/Examples.kt
new file mode 100644
index 0000000000..4c0a9045cb
--- /dev/null
+++ b/app/src/processing/app/ui/components/examples/Examples.kt
@@ -0,0 +1,194 @@
+package processing.app.ui.components.examples
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerIcon
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.decodeToImageBitmap
+import processing.app.LocalPreferences
+import processing.app.Messages
+import processing.app.Platform
+import processing.app.ui.Welcome.Companion.LocalBase
+import java.awt.Cursor
+import java.io.File
+import java.nio.file.*
+import java.nio.file.attribute.BasicFileAttributes
+import kotlin.io.path.exists
+import kotlin.io.path.inputStream
+import kotlin.io.path.isDirectory
+
+data class Example(
+ val folder: Path,
+ val library: Path,
+ val path: String = library.resolve("examples").relativize(folder).toString(),
+ val title: String = folder.fileName.toString(),
+ val image: Path = folder.resolve("$title.png")
+)
+
+@Composable
+fun loadExamples(): List {
+ val sketchbook = rememberSketchbookPath()
+ val resources = File(System.getProperty("compose.application.resources.dir") ?: "")
+ var examples by remember { mutableStateOf(emptyList()) }
+
+ val settingsFolder = Platform.getSettingsFolder()
+ val examplesCache = settingsFolder.resolve("examples.cache")
+ LaunchedEffect(sketchbook, resources){
+ if (!examplesCache.exists()) return@LaunchedEffect
+ withContext(Dispatchers.IO) {
+ examples = examplesCache.readText().lines().map {
+ val (library, folder) = it.split(",")
+ Example(
+ folder = File(folder).toPath(),
+ library = File(library).toPath()
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(sketchbook, resources){
+ withContext(Dispatchers.IO) {
+ // TODO: Optimize
+ Messages.log("Start scanning for examples in $sketchbook and $resources")
+ // Folders that can contain contributions with examples
+ val scanned = listOf("libraries", "examples", "modes")
+ .flatMap { listOf(sketchbook.resolve(it), resources.resolve(it)) }
+ .filter { it.exists() && it.isDirectory() }
+ // Find contributions within those folders
+ .flatMap { Files.list(it.toPath()).toList() }
+ .filter { Files.isDirectory(it) }
+ // Find examples within those contributions
+ .flatMap { library ->
+ val fs = FileSystems.getDefault()
+ val matcher = fs.getPathMatcher("glob:**/*.pde")
+ val exampleFolders = mutableListOf()
+ val examples = library.resolve("examples")
+ if (!Files.exists(examples) || !examples.isDirectory()) return@flatMap emptyList()
+
+ Files.walkFileTree(library, object : SimpleFileVisitor() {
+ override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
+ if (matcher.matches(file)) {
+ exampleFolders.add(file.parent)
+ }
+ return FileVisitResult.CONTINUE
+ }
+ })
+ return@flatMap exampleFolders.map { folder ->
+ Example(
+ folder,
+ library,
+ )
+ }
+ }
+ .filter { it.image.exists() }
+ Messages.log("Done scanning for examples in $sketchbook and $resources")
+ if(scanned.isEmpty()) return@withContext
+ examples = scanned
+ examplesCache.writeText(examples.joinToString("\n") { "${it.library},${it.folder}" })
+ }
+ }
+
+ return examples
+
+}
+
+@Composable
+fun rememberSketchbookPath(): File {
+ val preferences = LocalPreferences.current
+ val sketchbookPath = remember(preferences["sketchbook.path.four"]) {
+ preferences["sketchbook.path.four"] ?: Platform.getDefaultSketchbookFolder().toString()
+ }
+ return File(sketchbookPath)
+}
+
+
+
+@Composable
+fun examples(){
+ val examples = loadExamples()
+
+
+ var randoms = examples.shuffled().take(4)
+ if(randoms.size < 4){
+ randoms = randoms + List(4 - randoms.size) { Example(
+ folder = Paths.get(""),
+ library = Paths.get(""),
+ title = "Example",
+ image = ClassLoader.getSystemResource("default.png")?.toURI()?.let { Paths.get(it) } ?: Paths.get(""),
+ ) }
+ }
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ randoms.chunked(2).forEach { row ->
+ Row (
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ){
+ row.forEach { example ->
+ Example(example)
+ }
+ }
+ }
+ }
+}
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+fun Example(example: Example){
+ val base = LocalBase.current
+ Button(
+ onClick = {
+ base?.handleOpenExample("${example.folder}/${example.title}.pde", base.defaultMode)
+ },
+ contentPadding = PaddingValues(0.dp),
+ elevation = null,
+ shape = RectangleShape,
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = Color.Transparent,
+ contentColor = colors.onBackground
+ ),
+ ) {
+ Column(
+ modifier = Modifier
+ .width(185.dp)
+ ) {
+ val imageBitmap: ImageBitmap = remember(example.image) {
+ example.image.inputStream().readAllBytes().decodeToImageBitmap()
+ }
+ Image(
+ painter = BitmapPainter(imageBitmap),
+ contentDescription = example.title,
+ modifier = Modifier
+ .background(colors.primary)
+ .aspectRatio(16f / 9f)
+ )
+ Text(
+ example.title,
+ style = typography.body1,
+ maxLines = 1
+ )
+ }
+ }
+}
diff --git a/app/src/processing/app/ui/theme/Button.kt b/app/src/processing/app/ui/theme/Button.kt
new file mode 100644
index 0000000000..bec6dd3bcd
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Button.kt
@@ -0,0 +1,52 @@
+package processing.app.ui.theme
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerIcon
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.unit.dp
+import java.awt.Cursor
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun PDEButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
+ var hover by remember { mutableStateOf(false) }
+ val offset by animateFloatAsState(if (hover) -3f else 3f)
+
+ Box {
+ Box(
+ modifier = Modifier
+ .offset((-offset).dp, (offset).dp)
+ .matchParentSize()
+ .padding(vertical = 6.dp)
+ .background(colors.secondary)
+
+ )
+ Button(
+ onClick = onClick,
+ shape = RectangleShape,
+ contentPadding = PaddingValues(vertical = 8.dp, horizontal = 32.dp),
+ modifier = Modifier
+ .onPointerEvent(PointerEventType.Enter) {
+ hover = true
+ }
+ .onPointerEvent(PointerEventType.Exit) {
+ hover = false
+ }
+ .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))),
+ content = content
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Chip.kt b/app/src/processing/app/ui/theme/Chip.kt
new file mode 100644
index 0000000000..baab6e8ef9
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Chip.kt
@@ -0,0 +1,31 @@
+package processing.app.ui.theme
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material.Chip
+import androidx.compose.material.ChipDefaults
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun PDEChip(
+ onClick: () -> Unit = {},
+ leadingIcon: @Composable (() -> Unit)? = null,
+ content: @Composable RowScope.() -> Unit
+){
+ Chip(
+ onClick = onClick,
+ border = BorderStroke(1.dp, colors.secondary),
+ colors = ChipDefaults.chipColors(
+ backgroundColor = colors.background,
+ contentColor = colors.primaryVariant
+ ),
+ leadingIcon = leadingIcon,
+ modifier = Modifier,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Colors.kt b/app/src/processing/app/ui/theme/Colors.kt
new file mode 100644
index 0000000000..bf10672ccf
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Colors.kt
@@ -0,0 +1,20 @@
+package processing.app.ui.theme
+
+import androidx.compose.material.Colors
+import androidx.compose.ui.graphics.Color
+
+val PDELightColors = Colors(
+ primary = Color("#0F195A".toColorInt()),
+ primaryVariant = Color("#1F34AB".toColorInt()),
+ secondary = Color("#82AFFF".toColorInt()),
+ secondaryVariant = Color("#0468FF".toColorInt()),
+ background = Color("#FFFFFF".toColorInt()),
+ surface = Color("#C0D7FF".toColorInt()),
+ error = Color("#0F195A".toColorInt()),
+ onPrimary = Color("#FFFFFF".toColorInt()),
+ onSecondary = Color("#FFFFFF".toColorInt()),
+ onBackground = Color("#0F195A".toColorInt()),
+ onSurface = Color("#FFFFFF".toColorInt()),
+ onError = Color("#0F195A".toColorInt()),
+ isLight = true,
+)
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt
index 254c0946c1..a4fd9eecfc 100644
--- a/app/src/processing/app/ui/theme/Locale.kt
+++ b/app/src/processing/app/ui/theme/Locale.kt
@@ -1,24 +1,27 @@
package processing.app.ui.theme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.compositionLocalOf
-import processing.app.LocalPreferences
-import processing.app.Messages
-import processing.app.Platform
-import processing.app.PlatformStart
-import processing.app.watchFile
+import androidx.compose.runtime.*
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import processing.app.*
import java.io.File
import java.io.InputStream
import java.util.*
-class Locale(language: String = "") : Properties() {
+class Locale(language: String = "", val setLocale: (java.util.Locale) -> Unit) : Properties() {
+ var locale: java.util.Locale = java.util.Locale.getDefault()
+
init {
- val locale = java.util.Locale.getDefault()
- load(ClassLoader.getSystemResourceAsStream("PDE.properties"))
- load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream())
- load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream())
- load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream())
+ fun loadResourceUTF8(path: String) {
+ val stream = ClassLoader.getSystemResourceAsStream(path)
+ stream?.reader(charset = Charsets.UTF_8)?.use { reader ->
+ load(reader)
+ }
+ }
+ loadResourceUTF8("PDE.properties")
+ loadResourceUTF8("PDE_${locale.language}.properties")
+ loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties")
+ loadResourceUTF8("PDE_${language}.properties")
}
@Deprecated("Use get instead", ReplaceWith("get(key)"))
@@ -28,18 +31,40 @@ class Locale(language: String = "") : Properties() {
return value
}
operator fun get(key: String): String = getProperty(key, key)
+ fun set(locale: java.util.Locale) {
+ setLocale(locale)
+ }
}
-val LocalLocale = compositionLocalOf { Locale() }
+val LocalLocale = compositionLocalOf { error("No Locale Set") }
@Composable
fun LocaleProvider(content: @Composable () -> Unit) {
- PlatformStart()
+ remember {
+ Platform.init()
+ }
val settingsFolder = Platform.getSettingsFolder()
val languageFile = File(settingsFolder, "language.txt")
watchFile(languageFile)
+ var code by remember{ mutableStateOf(languageFile.readText().substring(0, 2)) }
+
+ fun setLocale(locale: java.util.Locale) {
+ java.util.Locale.setDefault(locale)
+ languageFile.writeText(locale.language)
+ code = locale.language
+ Language.reload()
+ }
+
+
+ val locale = Locale(code, ::setLocale)
+ Messages.log("Locale: $code")
+ val dir = when(locale["locale.direction"]) {
+ "rtl" -> LayoutDirection.Rtl
+ else -> LayoutDirection.Ltr
+ }
- val locale = Locale(languageFile.readText().substring(0, 2))
- CompositionLocalProvider(LocalLocale provides locale) {
- content()
+ CompositionLocalProvider(LocalLayoutDirection provides dir) {
+ CompositionLocalProvider(LocalLocale provides locale) {
+ content()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt
index 735d8e5b2a..fae16424fb 100644
--- a/app/src/processing/app/ui/theme/Theme.kt
+++ b/app/src/processing/app/ui/theme/Theme.kt
@@ -1,7 +1,6 @@
package processing.app.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -10,13 +9,13 @@ import androidx.compose.ui.graphics.Color
import processing.app.LocalPreferences
import processing.app.PreferencesProvider
import java.io.InputStream
-import java.util.Properties
+import java.util.*
class Theme(themeFile: String? = "") : Properties() {
init {
load(ClassLoader.getSystemResourceAsStream("theme.txt"))
- load(ClassLoader.getSystemResourceAsStream(themeFile) ?: InputStream.nullInputStream())
+ load(ClassLoader.getSystemResourceAsStream(themeFile ?: "") ?: InputStream.nullInputStream())
}
fun getColor(key: String): Color {
return Color(getProperty(key).toColorInt())
@@ -33,26 +32,27 @@ fun ProcessingTheme(
PreferencesProvider {
val preferences = LocalPreferences.current
val theme = Theme(preferences.getProperty("theme"))
- val colors = Colors(
- primary = theme.getColor("editor.gradient.top"),
- primaryVariant = theme.getColor("toolbar.button.pressed.field"),
- secondary = theme.getColor("editor.gradient.bottom"),
- secondaryVariant = theme.getColor("editor.scrollbar.thumb.pressed.color"),
- background = theme.getColor("editor.bgcolor"),
- surface = theme.getColor("editor.bgcolor"),
- error = theme.getColor("status.error.bgcolor"),
- onPrimary = theme.getColor("toolbar.button.enabled.field"),
- onSecondary = theme.getColor("toolbar.button.enabled.field"),
- onBackground = theme.getColor("editor.fgcolor"),
- onSurface = theme.getColor("editor.fgcolor"),
- onError = theme.getColor("status.error.fgcolor"),
- isLight = theme.getProperty("laf.mode").equals("light")
- )
+// val colors = Colors(
+// primary = theme.getColor("editor.gradient.top"),
+// primaryVariant = theme.getColor("toolbar.button.pressed.field"),
+// secondary = theme.getColor("editor.gradient.bottom"),
+// secondaryVariant = theme.getColor("editor.scrollbar.thumb.pressed.color"),
+// background = theme.getColor("editor.bgcolor"),
+// surface = theme.getColor("editor.bgcolor"),
+// error = theme.getColor("status.error.bgcolor"),
+// onPrimary = theme.getColor("toolbar.button.enabled.field"),
+// onSecondary = theme.getColor("toolbar.button.enabled.field"),
+// onBackground = theme.getColor("editor.fgcolor"),
+// onSurface = theme.getColor("editor.fgcolor"),
+// onError = theme.getColor("status.error.fgcolor"),
+// isLight = theme.getProperty("laf.mode").equals("light")
+// )
+
CompositionLocalProvider(LocalTheme provides theme) {
LocaleProvider {
MaterialTheme(
- colors = colors,
+ colors = if(darkTheme) PDELightColors else PDELightColors,
typography = Typography,
content = content
)
@@ -71,5 +71,8 @@ fun String.toColorInt(): Int {
}
return color.toInt()
}
+ if (this.startsWith("0x")) {
+ return this.substring(2).toInt(16)
+ }
throw IllegalArgumentException("Unknown color")
}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Typography.kt b/app/src/processing/app/ui/theme/Typography.kt
index 5d87c490e6..c21d554f7e 100644
--- a/app/src/processing/app/ui/theme/Typography.kt
+++ b/app/src/processing/app/ui/theme/Typography.kt
@@ -2,6 +2,8 @@ package processing.app.ui.theme
import androidx.compose.material.MaterialTheme.typography
import androidx.compose.material.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -21,18 +23,39 @@ val processingFont = FontFamily(
style = FontStyle.Normal
)
)
+val spaceGroteskFont = FontFamily(
+ Font(
+ resource = "SpaceGrotesk-Bold.ttf",
+ weight = FontWeight.Bold,
+ ),
+ Font(
+ resource = "SpaceGrotesk-Regular.ttf",
+ weight = FontWeight.Normal,
+ ),
+ Font(
+ resource = "SpaceGrotesk-Medium.ttf",
+ weight = FontWeight.Medium,
+ ),
+ Font(
+ resource = "SpaceGrotesk-SemiBold.ttf",
+ weight = FontWeight.SemiBold,
+ ),
+ Font(
+ resource = "SpaceGrotesk-Light.ttf",
+ weight = FontWeight.Light,
+ )
+)
val Typography = Typography(
+ defaultFontFamily = spaceGroteskFont,
+ h4 = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 19.sp,
+ lineHeight = 24.sp
+ ),
body1 = TextStyle(
- fontFamily = processingFont,
fontWeight = FontWeight.Normal,
- fontSize = 13.sp,
- lineHeight = 16.sp
+ fontSize = 15.sp,
+ lineHeight = 19.sp
),
- subtitle1 = TextStyle(
- fontFamily = processingFont,
- fontWeight = FontWeight.Bold,
- fontSize = 16.sp,
- lineHeight = 20.sp
- )
)
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt
new file mode 100644
index 0000000000..0cb419332c
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Window.kt
@@ -0,0 +1,106 @@
+package processing.app.ui.theme
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.awt.ComposePanel
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowPosition
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import com.formdev.flatlaf.util.SystemInfo
+
+import java.awt.event.KeyAdapter
+import java.awt.event.KeyEvent
+import javax.swing.JFrame
+
+val LocalWindow = compositionLocalOf { error("No Window Set") }
+
+class PDEWindow(titleKey: String = "", fullWindowContent: Boolean = false, content: @Composable () -> Unit): JFrame(){
+ init{
+ val mac = SystemInfo.isMacFullWindowContentSupported
+
+ rootPane.apply{
+ putClientProperty("apple.awt.transparentTitleBar", mac)
+ putClientProperty("apple.awt.fullWindowContent", mac)
+ }
+
+ defaultCloseOperation = DISPOSE_ON_CLOSE
+ ComposePanel().apply {
+ setContent {
+ CompositionLocalProvider(LocalWindow provides this@PDEWindow) {
+ ProcessingTheme {
+ val locale = LocalLocale.current
+ this@PDEWindow.title = locale[titleKey]
+ LaunchedEffect(locale) {
+ this@PDEWindow.pack()
+ this@PDEWindow.setLocationRelativeTo(null)
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
+ ) {
+ content()
+
+ }
+ }
+ }
+ }
+
+ this@PDEWindow.add(this)
+ }
+ pack()
+ background = java.awt.Color.white
+ setLocationRelativeTo(null)
+ addKeyListener(object : KeyAdapter() {
+ override fun keyPressed(e: KeyEvent) {
+ if (e.keyCode == KeyEvent.VK_ESCAPE) this@PDEWindow.dispose()
+ }
+ })
+ isResizable = false
+ isVisible = true
+ requestFocus()
+ }
+}
+
+fun pdeapplication(titleKey: String = "", fullWindowContent: Boolean = false,content: @Composable () -> Unit){
+ application {
+ val windowState = rememberWindowState(
+ size = DpSize.Unspecified,
+ position = WindowPosition(Alignment.Center)
+ )
+ ProcessingTheme {
+ val locale = LocalLocale.current
+ val mac = SystemInfo.isMacFullWindowContentSupported
+ Window(onCloseRequest = ::exitApplication, state = windowState, title = locale[titleKey]) {
+ window.rootPane.apply {
+ putClientProperty("apple.awt.fullWindowContent", mac)
+ putClientProperty("apple.awt.transparentTitleBar", mac)
+ }
+ LaunchedEffect(locale){
+ window.pack()
+ window.setLocationRelativeTo(null)
+ }
+ CompositionLocalProvider(LocalWindow provides window) {
+ Surface(color = colors.background) {
+ Box(
+ modifier = Modifier
+ .padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
+ ) {
+ content()
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/test/processing/app/PreferencesKtTest.kt b/app/test/processing/app/PreferencesKtTest.kt
new file mode 100644
index 0000000000..f38796668e
--- /dev/null
+++ b/app/test/processing/app/PreferencesKtTest.kt
@@ -0,0 +1,34 @@
+package processing.app
+
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.*
+import kotlin.test.Test
+
+class PreferencesKtTest{
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun testKeyReactivity() = runComposeUiTest {
+ val newValue = (0..Int.MAX_VALUE).random().toString()
+ val testKey = "test.preferences.reactivity.$newValue"
+ setContent {
+ PreferencesProvider {
+ val preferences = LocalPreferences.current
+ Text(preferences[testKey] ?: "default", modifier = Modifier.testTag("text"))
+
+ Button(onClick = {
+ preferences[testKey] = newValue
+ }, modifier = Modifier.testTag("button")) {
+ Text("Change")
+ }
+ }
+ }
+
+ onNodeWithTag("text").assertTextEquals("default")
+ onNodeWithTag("button").performClick()
+ onNodeWithTag("text").assertTextEquals(newValue)
+ }
+
+}
\ No newline at end of file
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/build.gradle.kts b/build.gradle.kts
index 0675c2db38..bee6f54e96 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,6 +6,21 @@ plugins {
alias(libs.plugins.jetbrainsCompose) apply false
}
+allprojects{
+ repositories{
+ maven { url = uri("https://repo.gradle.org/gradle/libs-releases") }
+ }
+}
+
// Set the build directory to not /build to prevent accidental deletion through the clean action
// Can be deleted after the migration to Gradle is complete
-layout.buildDirectory = file(".build")
\ No newline at end of file
+layout.buildDirectory = file(".build")
+
+allprojects{
+ tasks.withType {
+ options.encoding = "UTF-8"
+ }
+ tasks.withType {
+ options.encoding = "UTF-8"
+ }
+}
\ No newline at end of file
diff --git a/build/shared/include.jdk b/build/shared/include.jdk
new file mode 100644
index 0000000000..f98bc164d5
--- /dev/null
+++ b/build/shared/include.jdk
@@ -0,0 +1 @@
+This file is used by gradle to determine where it should copy the jdk
\ No newline at end of file
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf b/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf
new file mode 100644
index 0000000000..0408641c61
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt b/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
new file mode 100644
index 0000000000..6a314848b3
--- /dev/null
+++ b/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Light.ttf b/build/shared/lib/fonts/SpaceGrotesk-Light.ttf
new file mode 100644
index 0000000000..d41bcccd86
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Light.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf b/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf
new file mode 100644
index 0000000000..7d44b663b9
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf b/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf
new file mode 100644
index 0000000000..981bcf5b2c
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf b/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf
new file mode 100644
index 0000000000..e7e02e51e4
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf differ
diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties
index 9d03f33c08..3ea6d7652b 100644
--- a/build/shared/lib/languages/PDE.properties
+++ b/build/shared/lib/languages/PDE.properties
@@ -621,6 +621,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..9b63626e5d 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -34,10 +34,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 +88,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 9f3486a10d..2c88947b0d 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
@@ -9932,19 +9932,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/gradle.properties b/gradle.properties
new file mode 100644
index 0000000000..b1a11daba3
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+#version=4.12.1
+#group=nl.steftervelde
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a9fe0b6e52..f2dc0305ec 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,7 @@
kotlin = "2.0.20"
compose-plugin = "1.7.1"
jogl = "2.5.0"
+antlr = "4.13.2"
jupiter = "5.12.0"
[libraries]
@@ -25,6 +26,11 @@ netbeansSwing = { module = "org.netbeans.api:org-netbeans-swing-outline", versio
ant = { module = "org.apache.ant:ant", version = "1.10.14" }
lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version = "0.22.0" }
jsoup = { module = "org.jsoup:jsoup", version = "1.17.2" }
+antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr" }
+antlr4Runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" }
+composeGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" }
+kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlinComposePlugin = { module = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin", version.ref = "kotlin" }
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" }
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
@@ -34,4 +40,5 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref =
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
download = { id = "de.undercouch.download", version = "5.6.0" }
-mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
\ No newline at end of file
+mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
+gradlePublish = { id = "com.gradle.plugin-publish", version = "1.2.1" }
\ No newline at end of file
diff --git a/java/build.gradle.kts b/java/build.gradle.kts
index 0f8e052780..74052f2085 100644
--- a/java/build.gradle.kts
+++ b/java/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
+
plugins {
id("java")
}
@@ -53,7 +55,7 @@ tasks.register("extraResources"){
include("keywords.txt")
include("theme/**/*")
include("application/**/*")
- into( layout.buildDirectory.dir("resources-bundled/common/modes/java"))
+ into(layout.buildDirectory.dir("resources-bundled/common/modes/java"))
}
tasks.register("copyCore"){
val coreProject = project(":core")
@@ -64,6 +66,22 @@ tasks.register("copyCore"){
rename("core.+\\.jar", "core.jar")
into(coreProject.layout.projectDirectory.dir("library"))
}
+tasks.register("renameWindres") {
+ val dir = layout.buildDirectory.dir("resources-bundled/common/modes/java")
+ 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.named("extraResources"){ dependsOn(this) }
+}
val libraries = arrayOf("dxf","io","net","pdf","serial","svg")
libraries.forEach { library ->
@@ -77,7 +95,7 @@ libraries.forEach { library ->
include("*.properties")
include("library/**/*")
include("examples/**/*")
- into( layout.buildDirectory.dir("resources-bundled/common/modes/java/libraries/$library"))
+ into(layout.buildDirectory.dir("resources-bundled/common/modes/java/libraries/$library"))
}
tasks.named("extraResources"){ dependsOn("library-$library-extraResources") }
}
diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts
new file mode 100644
index 0000000000..3aa484cb50
--- /dev/null
+++ b/java/gradle/build.gradle.kts
@@ -0,0 +1,38 @@
+plugins{
+ `java-gradle-plugin`
+ alias(libs.plugins.gradlePublish)
+
+ kotlin("jvm") version libs.versions.kotlin
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies{
+ implementation(project(":java:preprocessor"))
+
+ implementation(libs.composeGradlePlugin)
+ implementation(libs.kotlinGradlePlugin)
+ implementation(libs.kotlinComposePlugin)
+
+ testImplementation(libs.junit)
+}
+
+gradlePlugin{
+ plugins{
+ create("processing"){
+ id = "org.processing.gradle"
+ 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/brightness.pde b/java/gradle/example/brightness.pde
new file mode 100644
index 0000000000..7ad2b08294
--- /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);
+ 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..4d1d983e56
--- /dev/null
+++ b/java/gradle/example/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins{
+ id("org.processing.gradle")
+}
\ 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/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
new file mode 100644
index 0000000000..21d411f129
--- /dev/null
+++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt
@@ -0,0 +1,224 @@
+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.util.*
+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?
+
+ // Grab the settings from the most likely location
+ var settingsFolder = (project.findProperty("processing.settings") as String?)?.let { File(it) }
+ if(settingsFolder == null) {
+ val osName = System.getProperty("os.name").lowercase()
+ if (osName.contains("win")) {
+ settingsFolder = File(System.getenv("APPDATA"), "Processing")
+ } else if (osName.contains("mac")) {
+ settingsFolder = File(System.getProperty("user.home"), "Library/Processing")
+ } else if (osName.contains("nix") || osName.contains("nux")) {
+ settingsFolder = File(System.getProperty("user.home"), ".processing")
+ }
+ }
+
+ val preferences = File(settingsFolder, "preferences.txt")
+ val prefs = Properties()
+ if(preferences.exists()) prefs.load(preferences.inputStream())
+ prefs.setProperty("export.application.fullscreen", "false")
+ prefs.setProperty("export.application.present", "false")
+ prefs.setProperty("export.application.stop", "false")
+ if(preferences.exists()) prefs.store(preferences.outputStream(), null)
+
+ val sketchbook = project.findProperty("processing.sketchbook") as String?
+ ?: prefs.getProperty("sketchbook.path.four")
+ ?: ("${System.getProperty("user.home")}/.processing")
+
+ // Apply the Java plugin to the Project
+ 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 the compose plugin to wrap the sketch in an executable
+ project.plugins.apply("org.jetbrains.compose")
+
+ // TODO: Do we need these?
+ // Add kotlin support
+ project.plugins.apply("org.jetbrains.kotlin.jvm")
+ // Add jetpack compose support
+ project.plugins.apply("org.jetbrains.kotlin.plugin.compose")
+
+ // Add the Processing core library (within Processing from the internal maven repo and outside from the internet)
+ project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}")
+
+ // Add the jars in the code folder
+ project.dependencies.add("implementation", project.fileTree("src").apply { include("**/code/*.jar") })
+
+ // Add JOGL and Gluegen dependencies
+ // TODO: Add only if user is compiling for P2D or P3D
+ // TODO: Would require adding this after pre-processing
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt-main:2.5.0")
+
+ // TODO: Only add the native dependencies for the platform the user is building for
+ // MacOS specific native dependencies
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all:2.5.0:natives-macosx-universal")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-macosx-universal")
+
+ // Windows specific native dependencies
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all:2.5.0:natives-windows-amd64")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-windows-amd64")
+
+ // Linux specific native dependencies
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all:2.5.0:natives-linux-amd64")
+ project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-linux-amd64")
+
+ // NativeWindow dependencies for all platforms
+ project.dependencies.add("implementation", "org.jogamp.jogl:nativewindow:2.5.0")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-macosx-universal")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-windows-amd64")
+ project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-linux-amd64")
+
+ // Add the repositories necessary for building the sketch
+ 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
+ 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.modules("java.management")
+ if(debugPort != null) {
+ application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=$debugPort")
+ }
+ }
+ }
+
+ // TODO: Add support for top level .java files
+
+ // 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) }
+ }
+
+ project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.all { sourceSet ->
+ // For each java source set (mostly main) add a new source set for the PDE files
+ val pdeSourceSet = objectFactory.newInstance(
+ DefaultPDESourceDirectorySet::class.java,
+ objectFactory.sourceDirectorySet("${sourceSet.name}.pde", "${sourceSet.name} Processing Source")
+ ).apply {
+ filter.include("**/*.pde")
+ filter.exclude("${project.layout.buildDirectory.asFile.get().name}/**")
+
+ srcDir("./")
+ srcDir("$workingDir/unsaved")
+ }
+ sourceSet.allSource.source(pdeSourceSet)
+
+ val outputDirectory = project.layout.buildDirectory.asFile.get().resolve( "generated/pde/" + sourceSet.name)
+ sourceSet.java.srcDir(outputDirectory)
+
+ val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE")
+ project.tasks.register(pdeTaskName, ProcessingTask::class.java) { task ->
+ task.description = "Processes the ${sourceSet.name} PDE"
+ task.source = pdeSourceSet
+ task.outputDirectory = outputDirectory
+ task.sketchName = sketchName
+ task.workingDir = workingDir
+ task.sketchBook = sketchbook
+ }
+ val depsTaskName = sourceSet.getTaskName("addDependencies", "PDE")
+ project.tasks.register(depsTaskName){ task ->
+ task.dependsOn(pdeTaskName)
+ task.doLast {
+ outputDirectory
+ .listFiles()
+ ?.filter { file -> file.name.endsWith(".dependencies") }
+ ?.map { file ->
+ val dependencies = file.readLines()
+ dependencies.forEach { path ->
+ project.dependencies.add("implementation", project.files(path))
+ }
+ }
+ }
+ }
+
+ 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/main/kotlin/ProcessingTask.kt b/java/gradle/src/main/kotlin/ProcessingTask.kt
new file mode 100644
index 0000000000..22874c33d2
--- /dev/null
+++ b/java/gradle/src/main/kotlin/ProcessingTask.kt
@@ -0,0 +1,127 @@
+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.util.concurrent.Callable
+import java.util.jar.JarFile
+import javax.inject.Inject
+
+
+// TODO: Rename to PDE Task
+// TODO: Generate sourcemaps
+abstract class ProcessingTask : SourceTask() {
+ @get:OutputDirectory
+ var outputDirectory: File? = null
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:IgnoreEmptyDirectories
+ @get:SkipWhenEmpty
+ open val stableSources: FileCollection = project.files(Callable { this.source })
+
+ @get:Input
+ @get:Optional
+ var workingDir: String? = null
+
+ @get:Input
+ var sketchName: String = "processing"
+
+ @get:Input
+ @get:Optional
+ var sketchBook: String? = null
+
+ @TaskAction
+ fun execute(inputChanges: InputChanges) {
+ // TODO: Allow pre-processor to run on individual files (future)
+ // TODO: Only compare file names from both defined roots (e.g. sketch.pde and folder/sketch.pde should both be included)
+
+ // 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
+ .sortedByDescending { it.lastModified() }
+ .first()
+ }
+ .joinToString("\n"){
+ it.readText()
+ }
+ val javaFile = File(outputDirectory, "$sketchName.java").bufferedWriter()
+
+ val meta = PdePreprocessor
+ .builderFor(sketchName)
+ .build()
+ .write(javaFile, combined)
+
+ javaFile.flush()
+ javaFile.close()
+
+ // TODO: Move scanning the libraries to a separate task to avoid running this every time
+ // TODO: Support library changes
+ // TODO: Add internal libraries (dxf, serial, etc..)
+ // Scan all the libaries in the sketchbook
+ val libraries = File(sketchBook, "libraries")
+ .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 of 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 object {
+ val name = file.name
+ val path = file
+ val classes = classes
+ }
+ }?: emptyList()
+
+ // Save the parsed jars and which folder
+ return@map object {
+ val name = folder.name
+ val path = folder
+ val jars = jars
+ }
+ }
+
+ // Loop over the import statements and find the library jars that provide those imports
+ val dependencies = mutableSetOf()
+ meta.importStatements.map { import ->
+ libraries?.map { library ->
+ library.jars.map { jar ->
+ jar.classes
+ .filter { className -> className.startsWith(import.packageName) }
+ .map { _ ->
+ dependencies.add(jar.path)
+ }
+ }
+ }
+ }
+ // Write the dependencies to a file
+ val deps = File(outputDirectory, "$sketchName.dependencies")
+ deps.writeText(dependencies.joinToString("\n") { it.absolutePath })
+
+ // TODO: Add to the dependencies
+ val renderer = meta.sketchRenderer
+ }
+
+ @get:Inject
+ open val deleter: Deleter
+ get() {
+ throw UnsupportedOperationException("Decorator takes care of injection")
+ }
+}
\ No newline at end of file
diff --git a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt
new file mode 100644
index 0000000000..722f073dc1
--- /dev/null
+++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt
@@ -0,0 +1,16 @@
+import org.gradle.api.Task
+import org.gradle.testfixtures.ProjectBuilder
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class ProcessingPluginTest{
+ @Test
+ fun testPluginAddsSketchTask(){
+ val project = ProjectBuilder.builder().build()
+ project.pluginManager.apply("org.processing.gradle")
+
+ assert(project.tasks.getByName("sketch") is Task)
+ }
+}
diff --git a/java/lsp/build.gradle.kts b/java/lsp/build.gradle.kts
index 63ac7ab721..393f30344e 100644
--- a/java/lsp/build.gradle.kts
+++ b/java/lsp/build.gradle.kts
@@ -33,7 +33,10 @@ dependencies{
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 Language Server")
diff --git a/java/preprocessor/build.gradle.kts b/java/preprocessor/build.gradle.kts
index c6855df06d..e902afe21d 100644
--- a/java/preprocessor/build.gradle.kts
+++ b/java/preprocessor/build.gradle.kts
@@ -1,7 +1,8 @@
import com.vanniktech.maven.publish.SonatypeHost
plugins{
- id("java")
+ java
+ antlr
alias(libs.plugins.mavenPublish)
}
@@ -14,23 +15,41 @@ repositories{
sourceSets{
main{
java{
- srcDirs("src/main/java", "../src/", "../generated/")
+ srcDirs("src/main/java", "../src/")
include("processing/mode/java/preproc/**/*", "processing/app/**/*")
}
}
-
+}
+afterEvaluate{
+ tasks.withType(Jar::class.java){
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ dependsOn(tasks.generateGrammarSource)
+ }
}
dependencies{
implementation(libs.antlr)
implementation(libs.eclipseJDT)
- implementation(project(":core"))
+ antlr(libs.antlr4)
+ implementation(libs.antlr4Runtime)
+}
+
+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 Pre-processor")
@@ -58,13 +77,4 @@ mavenPublishing{
developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git")
}
}
-}
-tasks.withType {
- duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-}
-tasks.compileJava{
- dependsOn("ant-preproc")
-}
-ant.importBuild("../build.xml"){ antTaskName ->
- "ant-$antTaskName"
}
\ No newline at end of file
diff --git a/java/preprocessor/src/main/antlr/JavaLexer.g4 b/java/preprocessor/src/main/antlr/JavaLexer.g4
new file mode 100644
index 0000000000..b924864ea2
--- /dev/null
+++ b/java/preprocessor/src/main/antlr/JavaLexer.g4
@@ -0,0 +1,235 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2013 Terence Parr, Sam Harwell
+ Copyright (c) 2017 Ivan Kochurkin (upgrade to Java 8)
+ Copyright (c) 2021 MichaΕ Lorek (upgrade to Java 11)
+ Copyright (c) 2022 MichaΕ Lorek (upgrade to Java 17)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false
+// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine
+// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true
+
+lexer grammar JavaLexer;
+
+// Keywords
+
+ABSTRACT : 'abstract';
+ASSERT : 'assert';
+BOOLEAN : 'boolean';
+BREAK : 'break';
+BYTE : 'byte';
+CASE : 'case';
+CATCH : 'catch';
+CHAR : 'char';
+CLASS : 'class';
+CONST : 'const';
+CONTINUE : 'continue';
+DEFAULT : 'default';
+DO : 'do';
+DOUBLE : 'double';
+ELSE : 'else';
+ENUM : 'enum';
+EXTENDS : 'extends';
+FINAL : 'final';
+FINALLY : 'finally';
+FLOAT : 'float';
+FOR : 'for';
+IF : 'if';
+GOTO : 'goto';
+IMPLEMENTS : 'implements';
+IMPORT : 'import';
+INSTANCEOF : 'instanceof';
+INT : 'int';
+INTERFACE : 'interface';
+LONG : 'long';
+NATIVE : 'native';
+NEW : 'new';
+PACKAGE : 'package';
+PRIVATE : 'private';
+PROTECTED : 'protected';
+PUBLIC : 'public';
+RETURN : 'return';
+SHORT : 'short';
+STATIC : 'static';
+STRICTFP : 'strictfp';
+SUPER : 'super';
+SWITCH : 'switch';
+SYNCHRONIZED : 'synchronized';
+THIS : 'this';
+THROW : 'throw';
+THROWS : 'throws';
+TRANSIENT : 'transient';
+TRY : 'try';
+VOID : 'void';
+VOLATILE : 'volatile';
+WHILE : 'while';
+
+// Module related keywords
+MODULE : 'module';
+OPEN : 'open';
+REQUIRES : 'requires';
+EXPORTS : 'exports';
+OPENS : 'opens';
+TO : 'to';
+USES : 'uses';
+PROVIDES : 'provides';
+WITH : 'with';
+TRANSITIVE : 'transitive';
+
+// Local Variable Type Inference
+VAR: 'var'; // reserved type name
+
+// Switch Expressions
+YIELD: 'yield'; // reserved type name from Java 14
+
+// Records
+RECORD: 'record';
+
+// Sealed Classes
+SEALED : 'sealed';
+PERMITS : 'permits';
+NON_SEALED : 'non-sealed';
+
+// Literals
+
+DECIMAL_LITERAL : ('0' | [1-9] (Digits? | '_'+ Digits)) [lL]?;
+HEX_LITERAL : '0' [xX] [0-9a-fA-F] ([0-9a-fA-F_]* [0-9a-fA-F])? [lL]?;
+OCT_LITERAL : '0' '_'* [0-7] ([0-7_]* [0-7])? [lL]?;
+BINARY_LITERAL : '0' [bB] [01] ([01_]* [01])? [lL]?;
+
+FLOAT_LITERAL:
+ (Digits '.' Digits? | '.' Digits) ExponentPart? [fFdD]?
+ | Digits (ExponentPart [fFdD]? | [fFdD])
+;
+
+HEX_FLOAT_LITERAL: '0' [xX] (HexDigits '.'? | HexDigits? '.' HexDigits) [pP] [+-]? Digits [fFdD]?;
+
+BOOL_LITERAL: 'true' | 'false';
+
+CHAR_LITERAL: '\'' (~['\\\r\n] | EscapeSequence) '\'';
+
+STRING_LITERAL: '"' (~["\\\r\n] | EscapeSequence)* '"';
+
+MULTI_STRING_LIT: '"""' (~[\\] | EscapeSequence)*? '"""';
+
+TEXT_BLOCK: '"""' [ \t]* [\r\n] (. | EscapeSequence)*? '"""';
+
+NULL_LITERAL: 'null';
+
+// Separators
+
+LPAREN : '(';
+RPAREN : ')';
+LBRACE : '{';
+RBRACE : '}';
+LBRACK : '[';
+RBRACK : ']';
+SEMI : ';';
+COMMA : ',';
+DOT : '.';
+
+// Operators
+
+ASSIGN : '=';
+GT : '>';
+LT : '<';
+BANG : '!';
+TILDE : '~';
+QUESTION : '?';
+COLON : ':';
+EQUAL : '==';
+LE : '<=';
+GE : '>=';
+NOTEQUAL : '!=';
+AND : '&&';
+OR : '||';
+INC : '++';
+DEC : '--';
+ADD : '+';
+SUB : '-';
+MUL : '*';
+DIV : '/';
+BITAND : '&';
+BITOR : '|';
+CARET : '^';
+MOD : '%';
+
+ADD_ASSIGN : '+=';
+SUB_ASSIGN : '-=';
+MUL_ASSIGN : '*=';
+DIV_ASSIGN : '/=';
+AND_ASSIGN : '&=';
+OR_ASSIGN : '|=';
+XOR_ASSIGN : '^=';
+MOD_ASSIGN : '%=';
+LSHIFT_ASSIGN : '<<=';
+RSHIFT_ASSIGN : '>>=';
+URSHIFT_ASSIGN : '>>>=';
+
+// Java 8 tokens
+
+ARROW : '->';
+COLONCOLON : '::';
+
+// Additional symbols not defined in the lexical specification
+
+AT : '@';
+ELLIPSIS : '...';
+
+// Whitespace and comments
+
+WS : [ \t\r\n\u000C]+ -> channel(HIDDEN);
+COMMENT : '/*' .*? '*/' -> channel(HIDDEN);
+LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN);
+
+// Identifiers
+
+IDENTIFIER: Letter LetterOrDigit*;
+
+// Fragment rules
+
+fragment ExponentPart: [eE] [+-]? Digits;
+
+fragment EscapeSequence:
+ '\\' 'u005c'? [btnfr"'\\]
+ | '\\' 'u005c'? ([0-3]? [0-7])? [0-7]
+ | '\\' 'u'+ HexDigit HexDigit HexDigit HexDigit
+;
+
+fragment HexDigits: HexDigit ((HexDigit | '_')* HexDigit)?;
+
+fragment HexDigit: [0-9a-fA-F];
+
+fragment Digits: [0-9] ([0-9_]* [0-9])?;
+
+fragment LetterOrDigit: Letter | [0-9];
+
+fragment Letter:
+ [a-zA-Z$_] // these are the "java letters" below 0x7F
+ | ~[\u0000-\u007F\uD800-\uDBFF] // covers all characters above 0x7F which are not a surrogate
+ | [\uD800-\uDBFF] [\uDC00-\uDFFF] // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF
+;
\ No newline at end of file
diff --git a/java/preprocessor/src/main/antlr/JavaParser.g4 b/java/preprocessor/src/main/antlr/JavaParser.g4
new file mode 100644
index 0000000000..d273fa8885
--- /dev/null
+++ b/java/preprocessor/src/main/antlr/JavaParser.g4
@@ -0,0 +1,826 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2013 Terence Parr, Sam Harwell
+ Copyright (c) 2017 Ivan Kochurkin (upgrade to Java 8)
+ Copyright (c) 2021 MichaΕ Lorek (upgrade to Java 11)
+ Copyright (c) 2022 MichaΕ Lorek (upgrade to Java 17)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false
+// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging
+
+parser grammar JavaParser;
+
+options {
+ tokenVocab = JavaLexer;
+}
+
+compilationUnit
+ : packageDeclaration? (importDeclaration | ';')* (typeDeclaration | ';')* EOF
+ | moduleDeclaration EOF
+ ;
+
+packageDeclaration
+ : annotation* PACKAGE qualifiedName ';'
+ ;
+
+importDeclaration
+ : IMPORT STATIC? qualifiedName ('.' '*')? ';'
+ ;
+
+typeDeclaration
+ : classOrInterfaceModifier* (
+ classDeclaration
+ | enumDeclaration
+ | interfaceDeclaration
+ | annotationTypeDeclaration
+ | recordDeclaration
+ )
+ ;
+
+modifier
+ : classOrInterfaceModifier
+ | NATIVE
+ | SYNCHRONIZED
+ | TRANSIENT
+ | VOLATILE
+ ;
+
+classOrInterfaceModifier
+ : annotation
+ | PUBLIC
+ | PROTECTED
+ | PRIVATE
+ | STATIC
+ | ABSTRACT
+ | FINAL // FINAL for class only -- does not apply to interfaces
+ | STRICTFP
+ | SEALED // Java17
+ | NON_SEALED // Java17
+ ;
+
+variableModifier
+ : FINAL
+ | annotation
+ ;
+
+classDeclaration
+ : CLASS identifier typeParameters? (EXTENDS typeType)? (IMPLEMENTS typeList)? (
+ PERMITS typeList
+ )? // Java17
+ classBody
+ ;
+
+typeParameters
+ : '<' typeParameter (',' typeParameter)* '>'
+ ;
+
+typeParameter
+ : annotation* identifier (EXTENDS annotation* typeBound)?
+ ;
+
+typeBound
+ : typeType ('&' typeType)*
+ ;
+
+enumDeclaration
+ : ENUM identifier (IMPLEMENTS typeList)? '{' enumConstants? ','? enumBodyDeclarations? '}'
+ ;
+
+enumConstants
+ : enumConstant (',' enumConstant)*
+ ;
+
+enumConstant
+ : annotation* identifier arguments? classBody?
+ ;
+
+enumBodyDeclarations
+ : ';' classBodyDeclaration*
+ ;
+
+interfaceDeclaration
+ : INTERFACE identifier typeParameters? (EXTENDS typeList)? (PERMITS typeList)? interfaceBody
+ ;
+
+classBody
+ : '{' classBodyDeclaration* '}'
+ ;
+
+interfaceBody
+ : '{' interfaceBodyDeclaration* '}'
+ ;
+
+classBodyDeclaration
+ : ';'
+ | STATIC? block
+ | modifier* memberDeclaration
+ ;
+
+memberDeclaration
+ : recordDeclaration //Java17
+ | methodDeclaration
+ | genericMethodDeclaration
+ | fieldDeclaration
+ | constructorDeclaration
+ | genericConstructorDeclaration
+ | interfaceDeclaration
+ | annotationTypeDeclaration
+ | classDeclaration
+ | enumDeclaration
+ ;
+
+/* We use rule this even for void methods which cannot have [] after parameters.
+ This simplifies grammar and we can consider void to be a type, which
+ renders the [] matching as a context-sensitive issue or a semantic check
+ for invalid return type after parsing.
+ */
+methodDeclaration
+ : typeTypeOrVoid identifier formalParameters ('[' ']')* (THROWS qualifiedNameList)? methodBody
+ ;
+
+methodBody
+ : block
+ | ';'
+ ;
+
+typeTypeOrVoid
+ : typeType
+ | VOID
+ ;
+
+genericMethodDeclaration
+ : typeParameters methodDeclaration
+ ;
+
+genericConstructorDeclaration
+ : typeParameters constructorDeclaration
+ ;
+
+constructorDeclaration
+ : identifier formalParameters (THROWS qualifiedNameList)? constructorBody = block
+ ;
+
+compactConstructorDeclaration
+ : modifier* identifier constructorBody = block
+ ;
+
+fieldDeclaration
+ : typeType variableDeclarators ';'
+ ;
+
+interfaceBodyDeclaration
+ : modifier* interfaceMemberDeclaration
+ | ';'
+ ;
+
+interfaceMemberDeclaration
+ : recordDeclaration // Java17
+ | constDeclaration
+ | interfaceMethodDeclaration
+ | genericInterfaceMethodDeclaration
+ | interfaceDeclaration
+ | annotationTypeDeclaration
+ | classDeclaration
+ | enumDeclaration
+ ;
+
+constDeclaration
+ : typeType constantDeclarator (',' constantDeclarator)* ';'
+ ;
+
+constantDeclarator
+ : identifier ('[' ']')* '=' variableInitializer
+ ;
+
+// Early versions of Java allows brackets after the method name, eg.
+// public int[] return2DArray() [] { ... }
+// is the same as
+// public int[][] return2DArray() { ... }
+interfaceMethodDeclaration
+ : interfaceMethodModifier* interfaceCommonBodyDeclaration
+ ;
+
+// Java8
+interfaceMethodModifier
+ : annotation
+ | PUBLIC
+ | ABSTRACT
+ | DEFAULT
+ | STATIC
+ | STRICTFP
+ ;
+
+genericInterfaceMethodDeclaration
+ : interfaceMethodModifier* typeParameters interfaceCommonBodyDeclaration
+ ;
+
+interfaceCommonBodyDeclaration
+ : annotation* typeTypeOrVoid identifier formalParameters ('[' ']')* (THROWS qualifiedNameList)? methodBody
+ ;
+
+variableDeclarators
+ : variableDeclarator (',' variableDeclarator)*
+ ;
+
+variableDeclarator
+ : variableDeclaratorId ('=' variableInitializer)?
+ ;
+
+variableDeclaratorId
+ : identifier ('[' ']')*
+ ;
+
+variableInitializer
+ : arrayInitializer
+ | expression
+ ;
+
+arrayInitializer
+ : '{' (variableInitializer (',' variableInitializer)* ','?)? '}'
+ ;
+
+classOrInterfaceType
+ : (identifier typeArguments? '.')* typeIdentifier typeArguments?
+ ;
+
+typeArgument
+ : typeType
+ | annotation* '?' ((EXTENDS | SUPER) typeType)?
+ ;
+
+qualifiedNameList
+ : qualifiedName (',' qualifiedName)*
+ ;
+
+formalParameters
+ : '(' (
+ receiverParameter?
+ | receiverParameter (',' formalParameterList)?
+ | formalParameterList?
+ ) ')'
+ ;
+
+receiverParameter
+ : typeType (identifier '.')* THIS
+ ;
+
+formalParameterList
+ : formalParameter (',' formalParameter)* (',' lastFormalParameter)?
+ | lastFormalParameter
+ ;
+
+formalParameter
+ : variableModifier* typeType variableDeclaratorId
+ ;
+
+lastFormalParameter
+ : variableModifier* typeType annotation* '...' variableDeclaratorId
+ ;
+
+// local variable type inference
+lambdaLVTIList
+ : lambdaLVTIParameter (',' lambdaLVTIParameter)*
+ ;
+
+lambdaLVTIParameter
+ : variableModifier* VAR identifier
+ ;
+
+qualifiedName
+ : identifier ('.' identifier)*
+ ;
+
+baseStringLiteral
+ : STRING_LITERAL
+ ;
+
+multilineStringLiteral
+ : MULTI_STRING_LIT
+ ;
+
+stringLiteral
+ : baseStringLiteral
+ | multilineStringLiteral
+ ;
+
+literal
+ : integerLiteral
+ | floatLiteral
+ | CHAR_LITERAL
+ | stringLiteral
+ | BOOL_LITERAL
+ | NULL_LITERAL
+ | TEXT_BLOCK // Java17
+ ;
+
+integerLiteral
+ : DECIMAL_LITERAL
+ | HEX_LITERAL
+ | OCT_LITERAL
+ | BINARY_LITERAL
+ ;
+
+floatLiteral
+ : FLOAT_LITERAL
+ | HEX_FLOAT_LITERAL
+ ;
+
+// ANNOTATIONS
+altAnnotationQualifiedName
+ : (identifier DOT)* '@' identifier
+ ;
+
+annotation
+ : ('@' qualifiedName | altAnnotationQualifiedName) (
+ '(' ( elementValuePairs | elementValue)? ')'
+ )?
+ ;
+
+elementValuePairs
+ : elementValuePair (',' elementValuePair)*
+ ;
+
+elementValuePair
+ : identifier '=' elementValue
+ ;
+
+elementValue
+ : expression
+ | annotation
+ | elementValueArrayInitializer
+ ;
+
+elementValueArrayInitializer
+ : '{' (elementValue (',' elementValue)*)? ','? '}'
+ ;
+
+annotationTypeDeclaration
+ : '@' INTERFACE identifier annotationTypeBody
+ ;
+
+annotationTypeBody
+ : '{' annotationTypeElementDeclaration* '}'
+ ;
+
+annotationTypeElementDeclaration
+ : modifier* annotationTypeElementRest
+ | ';' // this is not allowed by the grammar, but apparently allowed by the actual compiler
+ ;
+
+annotationTypeElementRest
+ : typeType annotationMethodOrConstantRest ';'
+ | classDeclaration ';'?
+ | interfaceDeclaration ';'?
+ | enumDeclaration ';'?
+ | annotationTypeDeclaration ';'?
+ | recordDeclaration ';'? // Java17
+ ;
+
+annotationMethodOrConstantRest
+ : annotationMethodRest
+ | annotationConstantRest
+ ;
+
+annotationMethodRest
+ : identifier '(' ')' defaultValue?
+ ;
+
+annotationConstantRest
+ : variableDeclarators
+ ;
+
+defaultValue
+ : DEFAULT elementValue
+ ;
+
+// MODULES - Java9
+
+moduleDeclaration
+ : OPEN? MODULE qualifiedName moduleBody
+ ;
+
+moduleBody
+ : '{' moduleDirective* '}'
+ ;
+
+moduleDirective
+ : REQUIRES requiresModifier* qualifiedName ';'
+ | EXPORTS qualifiedName (TO qualifiedName)? ';'
+ | OPENS qualifiedName (TO qualifiedName)? ';'
+ | USES qualifiedName ';'
+ | PROVIDES qualifiedName WITH qualifiedName ';'
+ ;
+
+requiresModifier
+ : TRANSITIVE
+ | STATIC
+ ;
+
+// RECORDS - Java 17
+
+recordDeclaration
+ : RECORD identifier typeParameters? recordHeader (IMPLEMENTS typeList)? recordBody
+ ;
+
+recordHeader
+ : '(' recordComponentList? ')'
+ ;
+
+recordComponentList
+ : recordComponent (',' recordComponent)*
+ ;
+
+recordComponent
+ : typeType identifier
+ ;
+
+recordBody
+ : '{' (classBodyDeclaration | compactConstructorDeclaration)* '}'
+ ;
+
+// STATEMENTS / BLOCKS
+
+block
+ : '{' blockStatement* '}'
+ ;
+
+blockStatement
+ : localVariableDeclaration ';'
+ | localTypeDeclaration
+ | statement
+ ;
+
+localVariableDeclaration
+ : variableModifier* (VAR identifier '=' expression | typeType variableDeclarators)
+ ;
+
+identifier
+ : IDENTIFIER
+ | MODULE
+ | OPEN
+ | REQUIRES
+ | EXPORTS
+ | OPENS
+ | TO
+ | USES
+ | PROVIDES
+ | WITH
+ | TRANSITIVE
+ | YIELD
+ | SEALED
+ | PERMITS
+ | RECORD
+ | VAR
+ ;
+
+typeIdentifier // Identifiers that are not restricted for type declarations
+ : IDENTIFIER
+ | MODULE
+ | OPEN
+ | REQUIRES
+ | EXPORTS
+ | OPENS
+ | TO
+ | USES
+ | PROVIDES
+ | WITH
+ | TRANSITIVE
+ | SEALED
+ | PERMITS
+ | RECORD
+ ;
+
+localTypeDeclaration
+ : classOrInterfaceModifier* (classDeclaration | interfaceDeclaration | recordDeclaration)
+ ;
+
+statement
+ : blockLabel = block
+ | ASSERT expression (':' expression)? ';'
+ | IF parExpression statement (ELSE statement)?
+ | FOR '(' forControl ')' statement
+ | WHILE parExpression statement
+ | DO statement WHILE parExpression ';'
+ | TRY block (catchClause+ finallyBlock? | finallyBlock)
+ | TRY resourceSpecification block catchClause* finallyBlock?
+ | SWITCH parExpression '{' switchBlockStatementGroup* switchLabel* '}'
+ | SYNCHRONIZED parExpression block
+ | RETURN expression? ';'
+ | THROW expression ';'
+ | BREAK identifier? ';'
+ | CONTINUE identifier? ';'
+ | YIELD expression ';' // Java17
+ | SEMI
+ | statementExpression = expression ';'
+ | switchExpression ';'? // Java17
+ | identifierLabel = identifier ':' statement
+ ;
+
+catchClause
+ : CATCH '(' variableModifier* catchType identifier ')' block
+ ;
+
+catchType
+ : qualifiedName ('|' qualifiedName)*
+ ;
+
+finallyBlock
+ : FINALLY block
+ ;
+
+resourceSpecification
+ : '(' resources ';'? ')'
+ ;
+
+resources
+ : resource (';' resource)*
+ ;
+
+resource
+ : variableModifier* (classOrInterfaceType variableDeclaratorId | VAR identifier) '=' expression
+ | qualifiedName
+ ;
+
+/** Matches cases then statements, both of which are mandatory.
+ * To handle empty cases at the end, we add switchLabel* to statement.
+ */
+switchBlockStatementGroup
+ : switchLabel+ blockStatement+
+ ;
+
+switchLabel
+ : CASE (
+ constantExpression = expression
+ | enumConstantName = IDENTIFIER
+ | typeType varName = identifier
+ ) ':'
+ | DEFAULT ':'
+ ;
+
+forControl
+ : enhancedForControl
+ | forInit? ';' expression? ';' forUpdate = expressionList?
+ ;
+
+forInit
+ : localVariableDeclaration
+ | expressionList
+ ;
+
+enhancedForControl
+ : variableModifier* (typeType | VAR) variableDeclaratorId ':' expression
+ ;
+
+// EXPRESSIONS
+
+parExpression
+ : '(' expression ')'
+ ;
+
+expressionList
+ : expression (',' expression)*
+ ;
+
+methodCall
+ : (identifier | THIS | SUPER) arguments
+ ;
+
+expression
+ // Expression order in accordance with https://introcs.cs.princeton.edu/java/11precedence/
+ // Level 16, Primary, array and member access
+ : primary #PrimaryExpression
+ | expression '[' expression ']' #SquareBracketExpression
+ | expression bop = '.' (
+ identifier
+ | methodCall
+ | THIS
+ | NEW nonWildcardTypeArguments? innerCreator
+ | SUPER superSuffix
+ | explicitGenericInvocation
+ ) #MemberReferenceExpression
+ // Method calls and method references are part of primary, and hence level 16 precedence
+ | methodCall #MethodCallExpression
+ | expression '::' typeArguments? identifier #MethodReferenceExpression
+ | typeType '::' (typeArguments? identifier | NEW) #MethodReferenceExpression
+ | classType '::' typeArguments? NEW #MethodReferenceExpression
+
+ // Java17
+ | switchExpression #ExpressionSwitch
+
+ // Level 15 Post-increment/decrement operators
+ | expression postfix = ('++' | '--') #PostIncrementDecrementOperatorExpression
+
+ // Level 14, Unary operators
+ | prefix = ('+' | '-' | '++' | '--' | '~' | '!') expression #UnaryOperatorExpression
+
+ // Level 13 Cast and object creation
+ | '(' annotation* typeType ('&' typeType)* ')' expression #CastExpression
+ | NEW creator #ObjectCreationExpression
+
+ // Level 12 to 1, Remaining operators
+ // Level 12, Multiplicative operators
+ | expression bop = ('*' | '/' | '%') expression #BinaryOperatorExpression
+ // Level 11, Additive operators
+ | expression bop = ('+' | '-') expression #BinaryOperatorExpression
+ // Level 10, Shift operators
+ | expression ('<' '<' | '>' '>' '>' | '>' '>') expression #BinaryOperatorExpression
+ // Level 9, Relational operators
+ | expression bop = ('<=' | '>=' | '>' | '<') expression #BinaryOperatorExpression
+ | expression bop = INSTANCEOF (typeType | pattern) #InstanceOfOperatorExpression
+ // Level 8, Equality Operators
+ | expression bop = ('==' | '!=') expression #BinaryOperatorExpression
+ // Level 7, Bitwise AND
+ | expression bop = '&' expression #BinaryOperatorExpression
+ // Level 6, Bitwise XOR
+ | expression bop = '^' expression #BinaryOperatorExpression
+ // Level 5, Bitwise OR
+ | expression bop = '|' expression #BinaryOperatorExpression
+ // Level 4, Logic AND
+ | expression bop = '&&' expression #BinaryOperatorExpression
+ // Level 3, Logic OR
+ | expression bop = '||' expression #BinaryOperatorExpression
+ // Level 2, Ternary
+ | expression bop = '?' expression ':' expression #TernaryExpression
+ // Level 1, Assignment
+ | expression bop = (
+ '='
+ | '+='
+ | '-='
+ | '*='
+ | '/='
+ | '&='
+ | '|='
+ | '^='
+ | '>>='
+ | '>>>='
+ | '<<='
+ | '%='
+ ) expression #BinaryOperatorExpression
+
+ // Level 0, Lambda Expression // Java8
+ | lambdaExpression #ExpressionLambda
+ ;
+
+// Java17
+pattern
+ : variableModifier* typeType annotation* identifier
+ ;
+
+// Java8
+lambdaExpression
+ : lambdaParameters '->' lambdaBody
+ ;
+
+// Java8
+lambdaParameters
+ : identifier
+ | '(' formalParameterList? ')'
+ | '(' identifier (',' identifier)* ')'
+ | '(' lambdaLVTIList? ')'
+ ;
+
+// Java8
+lambdaBody
+ : expression
+ | block
+ ;
+
+primary
+ : '(' expression ')'
+ | THIS
+ | SUPER
+ | literal
+ | identifier
+ | typeTypeOrVoid '.' CLASS
+ | nonWildcardTypeArguments (explicitGenericInvocationSuffix | THIS arguments)
+ ;
+
+// Java17
+switchExpression
+ : SWITCH parExpression '{' switchLabeledRule* '}'
+ ;
+
+// Java17
+switchLabeledRule
+ : CASE (expressionList | NULL_LITERAL | guardedPattern) (ARROW | COLON) switchRuleOutcome
+ | DEFAULT (ARROW | COLON) switchRuleOutcome
+ ;
+
+// Java17
+guardedPattern
+ : '(' guardedPattern ')'
+ | variableModifier* typeType annotation* identifier ('&&' expression)*
+ | guardedPattern '&&' expression
+ ;
+
+// Java17
+switchRuleOutcome
+ : block
+ | blockStatement*
+ ;
+
+classType
+ : (classOrInterfaceType '.')? annotation* identifier typeArguments?
+ ;
+
+creator
+ : nonWildcardTypeArguments? createdName classCreatorRest
+ | createdName arrayCreatorRest
+ ;
+
+createdName
+ : identifier typeArgumentsOrDiamond? ('.' identifier typeArgumentsOrDiamond?)*
+ | primitiveType
+ ;
+
+innerCreator
+ : identifier nonWildcardTypeArgumentsOrDiamond? classCreatorRest
+ ;
+
+arrayCreatorRest
+ : ('[' ']')+ arrayInitializer
+ | ('[' expression ']')+ ('[' ']')*
+ ;
+
+classCreatorRest
+ : arguments classBody?
+ ;
+
+explicitGenericInvocation
+ : nonWildcardTypeArguments explicitGenericInvocationSuffix
+ ;
+
+typeArgumentsOrDiamond
+ : '<' '>'
+ | typeArguments
+ ;
+
+nonWildcardTypeArgumentsOrDiamond
+ : '<' '>'
+ | nonWildcardTypeArguments
+ ;
+
+nonWildcardTypeArguments
+ : '<' typeList '>'
+ ;
+
+typeList
+ : typeType (',' typeType)*
+ ;
+
+typeType
+ : annotation* (classOrInterfaceType | primitiveType) (annotation* '[' ']')*
+ ;
+
+primitiveType
+ : BOOLEAN
+ | CHAR
+ | BYTE
+ | SHORT
+ | INT
+ | LONG
+ | FLOAT
+ | DOUBLE
+ ;
+
+typeArguments
+ : '<' typeArgument (',' typeArgument)* '>'
+ ;
+
+superSuffix
+ : arguments
+ | '.' typeArguments? identifier arguments?
+ ;
+
+explicitGenericInvocationSuffix
+ : SUPER superSuffix
+ | identifier arguments
+ ;
+
+arguments
+ : '(' expressionList? ')'
+ ;
\ No newline at end of file
diff --git a/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4 b/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4
new file mode 100644
index 0000000000..2d4edc041a
--- /dev/null
+++ b/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4
@@ -0,0 +1,147 @@
+/**
+ * Based on Java 1.7 grammar for ANTLR 4, see Java.g4
+ *
+ * - changes main entry point to reflect sketch types 'static' | 'active'
+ * - adds support for type converter functions like "int()"
+ * - adds pseudo primitive type "color"
+ * - adds HTML hex notation with hash symbol: #ff5522
+ * - allow color to appear as part of qualified names (like in imports)
+ */
+
+grammar Processing;
+
+@lexer::members {
+ public static final int WHITESPACE = 1;
+ public static final int COMMENTS = 2;
+}
+
+@header {
+ package processing.mode.java.preproc;
+}
+
+// import Java grammar
+import JavaParser, JavaLexer;
+
+// main entry point, select sketch type
+processingSketch
+ : staticProcessingSketch
+ | javaProcessingSketch
+ | activeProcessingSketch
+// | warnMixedModes
+ ;
+
+// java mode, is a compilation unit
+javaProcessingSketch
+ : packageDeclaration? importDeclaration* typeDeclaration+ EOF
+ ;
+
+// No method declarations, just statements
+staticProcessingSketch
+ : (importDeclaration | blockStatement | typeDeclaration)* EOF
+ ;
+
+// active mode, has function definitions
+activeProcessingSketch
+ : (importDeclaration | classBodyDeclaration)* EOF
+ ;
+
+// User incorrectly mixing modes. Included to allow for kind error message.
+warnMixedModes
+ : (importDeclaration | classBodyDeclaration | blockStatement)* blockStatement classBodyDeclaration (importDeclaration | classBodyDeclaration | blockStatement)*
+ | (importDeclaration | classBodyDeclaration | blockStatement)* classBodyDeclaration blockStatement (importDeclaration | classBodyDeclaration | blockStatement)*
+ ;
+
+variableDeclaratorId
+ : warnTypeAsVariableName
+ | IDENTIFIER ('[' ']')*
+ ;
+
+// bug #93
+// https://github.com/processing/processing/issues/93
+// prevent from types being used as variable names
+warnTypeAsVariableName
+ : primitiveType ('[' ']')* {
+ notifyErrorListeners("Type names are not allowed as variable names: "+$primitiveType.text);
+ }
+ ;
+
+// catch special API function calls that we are interested in
+methodCall
+ : functionWithPrimitiveTypeName
+ | IDENTIFIER '(' expressionList? ')'
+ | THIS '(' expressionList? ')'
+ | SUPER '(' expressionList? ')'
+ ;
+
+// these are primitive type names plus "()"
+// "color" is a special Processing primitive (== int)
+functionWithPrimitiveTypeName
+ : ( 'boolean'
+ | 'byte'
+ | 'char'
+ | 'float'
+ | 'int'
+ | 'color'
+ ) '(' expressionList? ')'
+ ;
+
+// adding support for "color" primitive
+primitiveType
+ : BOOLEAN
+ | CHAR
+ | BYTE
+ | SHORT
+ | INT
+ | LONG
+ | FLOAT
+ | DOUBLE
+ | colorPrimitiveType
+ ;
+
+colorPrimitiveType
+ : 'color'
+ ;
+
+qualifiedName
+ : (IDENTIFIER | colorPrimitiveType) ('.' (IDENTIFIER | colorPrimitiveType))*
+ ;
+
+// added HexColorLiteral
+literal
+ : integerLiteral
+ | floatLiteral
+ | CHAR_LITERAL
+ | stringLiteral
+ | BOOL_LITERAL
+ | NULL_LITERAL
+ | hexColorLiteral
+ ;
+
+// As parser rule so this produces a separate listener
+// for us to alter its value.
+hexColorLiteral
+ : HexColorLiteral
+ ;
+
+// add color literal notations for
+// #ff5522
+HexColorLiteral
+ : '#' (HexDigit HexDigit)? HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit
+ ;
+
+// hide but do not remove whitespace and comments
+
+WS : [ \t\r\n\u000C]+ -> channel(1)
+ ;
+
+COMMENT
+ : '/*' .*? '*/' -> channel(2)
+ ;
+
+LINE_COMMENT
+ : '//' ~[\r\n]* -> channel(2)
+ ;
+
+CHAR_LITERAL
+ : '\'' (~['\\\r\n] | EscapeSequence)* '\'' // A bit nasty but let JDT tackle invalid chars
+ ;
\ No newline at end of file
diff --git a/java/preprocessor/src/main/java/processing/app/Preferences.java b/java/preprocessor/src/main/java/processing/app/Preferences.java
index 7ce476fdea..eab3a23974 100644
--- a/java/preprocessor/src/main/java/processing/app/Preferences.java
+++ b/java/preprocessor/src/main/java/processing/app/Preferences.java
@@ -58,7 +58,7 @@ static public String get(String attribute /*, String defaultValue */) {
}
}
static public boolean getBoolean(String attribute) {
- String value = get(attribute); //, null);
+ String value = get(attribute);
return Boolean.parseBoolean(value);
}
static public int getInteger(String attribute /*, int defaultValue*/) {
diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java
index 815792955d..e3b3ef0941 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;
@@ -488,6 +487,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, () -> {
@@ -636,6 +639,10 @@ public void handleTweak() {
}
protected void handleLaunch(boolean present, boolean tweak) {
+ if(this.service.getEnabled()){
+ this.service.run();
+ return;
+ }
prepareRun();
toolbar.activateRun();
synchronized (runtimeLock) {
diff --git a/java/src/processing/mode/java/preproc/PdeParseTreeListener.java b/java/src/processing/mode/java/preproc/PdeParseTreeListener.java
index cb4fd00010..2f9580f787 100644
--- a/java/src/processing/mode/java/preproc/PdeParseTreeListener.java
+++ b/java/src/processing/mode/java/preproc/PdeParseTreeListener.java
@@ -32,7 +32,6 @@
import processing.app.Base;
import processing.app.Preferences;
-import processing.core.PApplet;
import processing.mode.java.preproc.PdePreprocessor.Mode;
/**
@@ -1237,16 +1236,16 @@ protected void writeMain(PrintWriterWithEditGen footerWriter,
boolean shouldFullScreen = Preferences.getBoolean("export.application.present");
shouldFullScreen = shouldFullScreen || Preferences.getBoolean("export.application.fullscreen");
if (shouldFullScreen) {
- argsJoiner.add("\"" + PApplet.ARGS_FULL_SCREEN + "\"");
+ argsJoiner.add("\"--full-screen\"");
String bgColor = Preferences.get("run.present.bgcolor");
- argsJoiner.add("\"" + PApplet.ARGS_BGCOLOR + "=" + bgColor + "\"");
+ argsJoiner.add("\"--bgcolor=" + bgColor + "\"");
if (Preferences.getBoolean("export.application.stop")) {
String stopColor = Preferences.get("run.present.stop.color");
- argsJoiner.add("\"" + PApplet.ARGS_STOP_COLOR + "=" + stopColor + "\"");
+ argsJoiner.add("\"--stop-color=" + stopColor + "\"");
} else {
- argsJoiner.add("\"" + PApplet.ARGS_HIDE_STOP + "\"");
+ argsJoiner.add("\"--hide-stop\"");
}
}
diff --git a/java/src/processing/mode/java/preproc/TextTransform.java b/java/src/processing/mode/java/preproc/TextTransform.java
index 77ae022f19..19ba8f3e53 100644
--- a/java/src/processing/mode/java/preproc/TextTransform.java
+++ b/java/src/processing/mode/java/preproc/TextTransform.java
@@ -8,8 +8,6 @@
import java.util.ListIterator;
import java.util.stream.Collectors;
-import processing.core.PApplet;
-
public class TextTransform {
@@ -256,7 +254,7 @@ public int getInputOffset(int outputOffset) {
i = -(i + 1);
i -= 1;
}
- i = PApplet.constrain(i, 0, outMap.size()-1);
+ i = constrain(i, 0, outMap.size()-1);
Edit edit = outMap.get(i);
int diff = outputOffset - edit.toOffset;
return edit.fromOffset + Math.min(diff, Math.max(0, edit.fromLength - 1));
@@ -271,7 +269,7 @@ public int getOutputOffset(int inputOffset) {
i = -(i + 1);
i -= 1;
}
- i = PApplet.constrain(i, 0, inMap.size()-1);
+ i = constrain(i, 0, inMap.size()-1);
Edit edit = inMap.get(i);
int diff = inputOffset - edit.fromOffset;
return edit.toOffset + Math.min(diff, Math.max(0, edit.toLength - 1));
@@ -283,6 +281,10 @@ public OffsetMapper thenMapping(OffsetMapper mapper) {
}
}
+ static public final int constrain(int amt, int low, int high) {
+ return (amt < low) ? low : ((amt > high) ? high : amt);
+ }
+
private static class CompositeOffsetMapper implements OffsetMapper {
private List mappers = new ArrayList<>();
diff --git a/java/test/resources/bug1532.pde b/java/test/resources/bug1532.pde
index 66b24b7779..ae8ecdbf8b 100644
--- a/java/test/resources/bug1532.pde
+++ b/java/test/resources/bug1532.pde
@@ -20,9 +20,9 @@ flatCube[][] grid;
void setup() {
try {
- quicktime.QTSession.open();
- }
- catch (quicktime.QTException qte) {
+ // quicktime.QTSession.open();
+ }
+ catch (quicktime.QTException qte) {
qte.printStackTrace();
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4bdcd880e8..5809665d7c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -5,6 +5,7 @@ include(
"app",
"java",
"java:preprocessor",
+ "java:gradle",
"java:libraries:dxf",
"java:libraries:io",
"java:libraries:net",