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