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..c4ffaff4de 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,6 @@
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
@@ -59,7 +60,7 @@ compose.desktop {
).map { "-D${it.first}=${it.second}" }.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"
@@ -109,6 +110,7 @@ dependencies {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+ implementation(compose.materialIconsExtended)
implementation(compose.desktop.currentOs)
@@ -121,6 +123,9 @@ dependencies {
testImplementation(libs.mockitoKotlin)
testImplementation(libs.junitJupiter)
testImplementation(libs.junitJupiterParams)
+
+ @OptIn(ExperimentalComposeLibrary::class)
+ testImplementation(compose.uiTest)
}
tasks.test {
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/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/Preferences.kt b/app/src/processing/app/Preferences.kt
index c5645c9bbc..a31cef2bbe 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()
@@ -62,12 +106,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/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..efa97d37cc
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Colors.kt
@@ -0,0 +1,33 @@
+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,
+)
+
+fun String.toColorInt(): Int {
+ if (this[0] == '#') {
+ var color = substring(1).toLong(16)
+ if (length == 7) {
+ color = color or 0x00000000ff000000L
+ } else if (length != 9) {
+ throw IllegalArgumentException("Unknown color")
+ }
+ return color.toInt()
+ }
+ throw IllegalArgumentException("Unknown color")
+}
\ 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..aee7abe00f 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
@@ -16,7 +15,7 @@ import java.util.Properties
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,43 +32,31 @@ 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
)
}
}
}
-}
-
-fun String.toColorInt(): Int {
- if (this[0] == '#') {
- var color = substring(1).toLong(16)
- if (length == 7) {
- color = color or 0x00000000ff000000L
- } else if (length != 9) {
- throw IllegalArgumentException("Unknown color")
- }
- return color.toInt()
- }
- 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/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