diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index a083d139a0..49a8625e51 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -198,6 +198,8 @@ static private void createAndShowGUI(String[] args) { // run static initialization that grabs all the prefs Preferences.init(); + PreferencesEvents.onUpdated(Preferences::init); + // boolean flag indicating whether to create new server instance or not boolean createNewInstance = DEBUG || !SingleInstance.alreadyRunning(args); @@ -485,6 +487,7 @@ public Base(String[] args) throws Exception { buildCoreModes(); rebuildContribModes(); rebuildContribExamples(); + rebuildToolList(); // Needs to happen after the sketchbook folder has been located. // Also relies on the modes to be loaded, so it knows what can be diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index bd75896afa..1b344a5e11 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -1,12 +1,9 @@ package processing.app import androidx.compose.runtime.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.dropWhile -import kotlinx.coroutines.launch import processing.utils.Settings import java.io.File import java.io.InputStream @@ -103,15 +100,17 @@ fun PreferencesProvider(content: @Composable () -> Unit) { ReactiveProperties().apply { val defaultsStream = ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: InputStream.nullInputStream() - load( - defaultsStream - .reader(Charsets.UTF_8) - ) - load( - preferencesFile - .inputStream() - .reader(Charsets.UTF_8) - ) + defaultsStream + .reader(Charsets.UTF_8) + .use { reader -> + load(reader) + } + preferencesFile + .inputStream() + .reader(Charsets.UTF_8) + .use { reader -> + load(reader) + } } } @@ -132,9 +131,9 @@ fun PreferencesProvider(content: @Composable () -> Unit) { .joinToString("\n") { (key, value) -> "$key=$value" } .toByteArray() ) - - // Reload legacy Preferences - Preferences.init() + output.close() + + PreferencesEvents.updated() } } } @@ -202,4 +201,19 @@ fun watchFile(file: File): Any? { } } return event +} + +class PreferencesEvents { + companion object { + val updatedListeners = mutableListOf() + + @JvmStatic + fun onUpdated(callback: Runnable) { + updatedListeners.add(callback) + } + + fun updated() { + updatedListeners.forEach { it.run() } + } + } } \ No newline at end of file diff --git a/app/src/processing/app/contrib/ContributionListing.java b/app/src/processing/app/contrib/ContributionListing.java index 08b8d307c7..b5937993c0 100644 --- a/app/src/processing/app/contrib/ContributionListing.java +++ b/app/src/processing/app/contrib/ContributionListing.java @@ -21,14 +21,6 @@ */ package processing.app.contrib; -import java.awt.EventQueue; -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.net.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; - import processing.app.Base; import processing.app.Messages; import processing.app.UpdateCheck; @@ -37,6 +29,16 @@ import processing.data.StringDict; import processing.data.StringList; +import java.awt.*; +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + public class ContributionListing { static volatile ContributionListing singleInstance; @@ -275,6 +277,8 @@ public void downloadAvailableList(final Base base, } catch (MalformedURLException e) { progress.setException(e); progress.finished(); + } catch (Exception e) { + Messages.log(e.getMessage()); } finally { downloadingLock.unlock(); } diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index 0437240b37..3ef108a27d 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -371,6 +371,7 @@ public void actionPerformed(ActionEvent e) { }); } + PreferencesEvents.onUpdated(this::updateTheme); } diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index ac5bf2609b..020856b8d4 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -99,7 +99,7 @@ class PDEPreferences { Interface.register() Coding.register() Sketches.register() - Other.register(panes) + Other.register() } /** @@ -111,6 +111,8 @@ class PDEPreferences { val locale = LocalLocale.current var preferencesQuery by remember { mutableStateOf("") } + Other.handleOtherPreferences(panes) + /** * Filter panes based on the search query. */ @@ -441,7 +443,7 @@ private val LocalModifiablePreferences = compositionLocalOf { ModifiablePreference(null, false, { }, {}) } /** - * Composable function that provides a modifiable copy of the current preferences. + * Composable function that captures an initial copy of the current preferences. * This allows for temporary changes to preferences that can be reset or applied later. * * @param content The composable content that will have access to the modifiable preferences. @@ -498,13 +500,13 @@ private fun CapturePreferences(content: @Composable () -> Unit) { } val apply = { - modified.entries.forEach { (key, value) -> - prefs.setProperty(key as String, (value ?: "") as String) + prefs.entries.forEach { (key, value) -> + modified.setProperty(key as String, (value ?: "") as String) } } val reset = { modified.entries.forEach { (key, value) -> - modified.setProperty(key as String, prefs[key] ?: "") + prefs.setProperty(key as String, modified[key] ?: "") } } val state = ModifiablePreference( @@ -515,7 +517,6 @@ private fun CapturePreferences(content: @Composable () -> Unit) { ) CompositionLocalProvider( - LocalPreferences provides modified, LocalModifiablePreferences provides state ) { content() diff --git a/app/src/processing/app/ui/Start.kt b/app/src/processing/app/ui/Start.kt index d7ed635ecf..a5edc845ed 100644 --- a/app/src/processing/app/ui/Start.kt +++ b/app/src/processing/app/ui/Start.kt @@ -14,10 +14,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +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 processing.app.Base +import java.awt.AWTEvent +import java.awt.event.WindowEvent + /** * Show a splash screen window. A rewrite of Splash.java @@ -27,8 +31,6 @@ class Start { @JvmStatic fun main(args: Array) { val duration = 200 - val timeMargin = 50 - application { var starting by remember { mutableStateOf(true) } Window( @@ -44,24 +46,10 @@ class Start { ) ) { var visible by remember { mutableStateOf(false) } - val composition = rememberCoroutineScope() + var launched by remember { mutableStateOf(false) } LaunchedEffect(Unit) { Toolkit.setIcon(window) - visible = true - composition.launch { - delay(duration.toLong() + timeMargin) - try { - Base.main(args) - } catch (e: Exception) { - throw InternalError("Failed to invoke main method", e) - } - composition.launch { - visible = false - delay(duration.toLong() + timeMargin) - starting = false - } - } } AnimatedVisibility( visible = visible, @@ -76,8 +64,23 @@ class Start { durationMillis = duration, easing = LinearEasing ) - ) + ), ) { + LaunchedEffect(visible, transition.currentState) { + if (launched) return@LaunchedEffect + if (!visible) return@LaunchedEffect + // Wait until the view is no longer transitioning + if (transition.targetState != transition.currentState) return@LaunchedEffect + launched = true + Base.main(args) + // List for any new windows opening, and close the splash when one does + java.awt.Toolkit.getDefaultToolkit() + .addAWTEventListener({ event -> + if (event.id != WindowEvent.WINDOW_OPENED) return@addAWTEventListener + + starting = false + }, AWTEvent.WINDOW_EVENT_MASK); + } Image( painter = painterResource("about-processing.svg"), contentDescription = "About", diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index a8bd559033..14b7e65787 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -46,6 +46,7 @@ class General { modifier = Modifier.fillMaxWidth(), label = { Text(locale["preferences.sketchbook_location"]) }, value = preference ?: "", + singleLine = true, onValueChange = { updatePreference(it) }, diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt index 8544f76945..c06058d724 100644 --- a/app/src/processing/app/ui/preferences/Other.kt +++ b/app/src/processing/app/ui/preferences/Other.kt @@ -6,6 +6,7 @@ import androidx.compose.material.icons.filled.Lightbulb import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -27,7 +28,7 @@ class Other { after = sketches ) - fun register(panes: PDEPreferencePanes) { + fun register() { PDEPreferences.register( PDEPreference( key = "preferences.show_other", @@ -41,56 +42,61 @@ class Other { setPreference(it.toString()) } ) - if (!showOther) { - return@PDEPreference - } - val prefs = LocalPreferences.current - val locale = LocalLocale.current - DisposableEffect(Unit) { - // add all the other options to the same group as the current one - val group = - panes[other]?.find { group -> group.any { preference -> preference.key == "preferences.show_other" } } as? MutableList + } + ) + ) + } - val existing = panes.values.flatten().flatten().map { preference -> preference.key } - val keys = prefs.keys.mapNotNull { it as? String }.filter { it !in existing }.sorted() + @Composable + fun handleOtherPreferences(panes: PDEPreferencePanes) { + // This function can be used to handle any specific logic related to other preferences if needed + val prefs = LocalPreferences.current + val locale = LocalLocale.current + if (prefs["preferences.show_other"]?.toBoolean() != true) { + return + } + DisposableEffect(panes) { + // add all the other options to the same group as the current one + val group = + panes[other]?.find { group -> group.any { preference -> preference.key == "preferences.show_other" } } as? MutableList - for (prefKey in keys) { - val descriptionKey = "preferences.$prefKey" - val preference = PDEPreference( - key = prefKey, - descriptionKey = if (locale.containsKey(descriptionKey)) descriptionKey else prefKey, - pane = other, - control = { preference, updatePreference -> - if (preference?.toBooleanStrictOrNull() != null) { - Switch( - checked = preference.toBoolean(), - onCheckedChange = { - updatePreference(it.toString()) - } - ) - return@PDEPreference - } + val existing = panes.values.flatten().flatten().map { preference -> preference.key } + val keys = prefs.keys.mapNotNull { it as? String }.filter { it !in existing }.sorted() - OutlinedTextField( - modifier = Modifier.widthIn(max = 300.dp), - value = preference ?: "", - onValueChange = { - updatePreference(it) - } - ) + for (prefKey in keys) { + val descriptionKey = "preferences.$prefKey" + val preference = PDEPreference( + key = prefKey, + descriptionKey = if (locale.containsKey(descriptionKey)) descriptionKey else prefKey, + pane = other, + control = { preference, updatePreference -> + if (preference?.toBooleanStrictOrNull() != null) { + Switch( + checked = preference.toBoolean(), + onCheckedChange = { + updatePreference(it.toString()) } ) - group?.add(preference) + return@PDEPreference } - onDispose { - group?.apply { - removeIf { it.key != "preferences.show_other" } + + OutlinedTextField( + modifier = Modifier.widthIn(max = 300.dp), + value = preference ?: "", + onValueChange = { + updatePreference(it) } - } + ) } + ) + group?.add(preference) + } + onDispose { + group?.apply { + removeIf { it.key != "preferences.show_other" } } - ) - ) + } + } } } } \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/Sketches.kt b/app/src/processing/app/ui/preferences/Sketches.kt index b3fef23cd0..f6754705d5 100644 --- a/app/src/processing/app/ui/preferences/Sketches.kt +++ b/app/src/processing/app/ui/preferences/Sketches.kt @@ -118,6 +118,7 @@ class Sketches { enabled = LocalPreferences.current["run.options.memory"]?.toBoolean() ?: false, modifier = Modifier.widthIn(max = 300.dp), value = preference ?: "", + singleLine = true, trailingIcon = { Text("MB") }, onValueChange = { setPreference(it) diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 74a410afc6..90de50a712 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -7,7 +7,6 @@ import processing.app.Messages import processing.app.watchFile import processing.utils.Settings import java.io.File -import java.io.InputStream import java.util.* /** @@ -26,21 +25,18 @@ import java.util.* class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() { var locale: java.util.Locale = java.util.Locale.getDefault() + fun loadResource(resourcePath: String) { + val stream = ClassLoader.getSystemResourceAsStream(resourcePath) ?: return + load(stream.reader(Charsets.UTF_8)) + } + init { - val locale = java.util.Locale.getDefault() - load(ClassLoader.getSystemResourceAsStream("languages/PDE.properties")) - load( - ClassLoader.getSystemResourceAsStream("languages/PDE_${locale.language}.properties") - ?: InputStream.nullInputStream() - ) - load( - ClassLoader.getSystemResourceAsStream("languages/PDE_${locale.toLanguageTag()}.properties") - ?: InputStream.nullInputStream() - ) - load( - ClassLoader.getSystemResourceAsStream("languages/PDE_${language}.properties") - ?: InputStream.nullInputStream() - ) + loadResource("languages/PDE.properties") + loadResource("languages/PDE_${locale.language}.properties") + loadResource("languages/PDE_${locale.toLanguageTag()}.properties") + if (language.isNotEmpty()) { + loadResource("languages/PDE_${language}.properties") + } } @Deprecated("Use get instead", ReplaceWith("get(key)")) @@ -66,6 +62,7 @@ class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? * ``` */ val LocalLocale = compositionLocalOf { error("No Locale Set") } +var LastLocaleUpdate by mutableStateOf(0L) /** * This composable function sets up a locale provider that manages application localization. @@ -105,7 +102,11 @@ fun LocaleProvider(content: @Composable () -> Unit) { } val update = watchFile(languageFile) - var code by remember(languageFile, update) { mutableStateOf(languageFile.readText().substring(0, 2)) } + var code by remember(languageFile, update, LastLocaleUpdate) { + mutableStateOf( + languageFile.readText().substring(0, 2) + ) + } remember(code) { val locale = java.util.Locale(code) java.util.Locale.setDefault(locale) @@ -115,6 +116,7 @@ fun LocaleProvider(content: @Composable () -> Unit) { Messages.log("Setting locale to ${locale.language}") languageFile.writeText(locale.language) code = locale.language + LastLocaleUpdate = System.currentTimeMillis() } diff --git a/java/build.gradle.kts b/java/build.gradle.kts index fc7151189c..47fa76e46b 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -68,7 +68,7 @@ tasks.register("copyCore"){ into(coreProject.layout.projectDirectory.dir("library")) } -val legacyLibraries = arrayOf("io","net","svg") +val legacyLibraries = arrayOf("io","net") legacyLibraries.forEach { library -> tasks.register("library-$library-extraResources"){ val build = project(":java:libraries:$library").tasks.named("build") @@ -87,7 +87,7 @@ legacyLibraries.forEach { library -> } } -val libraries = arrayOf("dxf", "pdf", "serial") +val libraries = arrayOf("dxf", "pdf", "serial", "svg") libraries.forEach { library -> val name = "create-$library-library" diff --git a/java/libraries/svg/build.gradle.kts b/java/libraries/svg/build.gradle.kts index a176f03df7..ddc4397842 100644 --- a/java/libraries/svg/build.gradle.kts +++ b/java/libraries/svg/build.gradle.kts @@ -1 +1,40 @@ -ant.importBuild("build.xml") \ No newline at end of file +plugins { + java +} + +sourceSets { + main { + java { + srcDirs("src") + } + } +} +repositories { + mavenCentral() +} + +dependencies { + compileOnly(project(":core")) + + implementation("org.apache.xmlgraphics:batik-all:1.19") +} + +tasks.register("createLibrary") { + dependsOn("jar") + into(layout.buildDirectory.dir("library")) + + from(layout.projectDirectory) { + include("library.properties") + include("examples/**") + } + + from(configurations.runtimeClasspath) { + into("library") + } + + from(tasks.jar) { + into("library") + rename { "svg.jar" } + } +} +