mirror of
https://github.com/processing/processing4.git
synced 2026-06-16 04:26:26 +02:00
Welcome Screen: Language selection
This commit is contained in:
@@ -21,6 +21,7 @@ tasks.withType<JavaExec> {
|
||||
systemProperty("processing.contributions.source", "https://contributions-preview.processing.org/contribs.txt")
|
||||
systemProperty("processing.download.page", "https://processing.org/download/")
|
||||
systemProperty("processing.download.latest", "https://processing.org/download/latest.txt")
|
||||
systemProperty("processing.tutorials", "https://processing.org/tutorials/")
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +96,7 @@ dependencies {
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
implementation(compose.desktop.currentOs)
|
||||
|
||||
|
||||
@@ -2,135 +2,90 @@ 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.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
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.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
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.res.loadImageBitmap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||
import processing.app.Base
|
||||
import processing.app.LocalPreferences
|
||||
import processing.app.Messages
|
||||
import processing.app.Platform
|
||||
import processing.app.*
|
||||
import processing.app.ui.components.LanguageChip
|
||||
import processing.app.ui.components.examples.examples
|
||||
import processing.app.ui.theme.*
|
||||
import java.awt.Cursor
|
||||
import java.io.File
|
||||
import java.awt.Desktop
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.*
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
|
||||
class Welcome @Throws(IOException::class) constructor(base: Base) {
|
||||
init {
|
||||
SwingUtilities.invokeLater {
|
||||
PDEWindow("menu.help.welcome", fullWindowContent = true) {
|
||||
welcome()
|
||||
CompositionLocalProvider(LocalBase provides base) {
|
||||
welcome()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LocalBase = compositionLocalOf<Base?> { null }
|
||||
@Composable
|
||||
fun welcome() {
|
||||
Row(
|
||||
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)
|
||||
end = Offset(815f * 2, 450f)
|
||||
)
|
||||
)
|
||||
.size(815.dp, 500.dp)
|
||||
.padding(32.dp)
|
||||
.padding(top = if(SystemInfo.isMacFullWindowContentSupported) 22.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.padding(top = if (SystemInfo.isMacFullWindowContentSupported) 22.dp else 0.dp),
|
||||
){
|
||||
Box(modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
){
|
||||
intro()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
) {
|
||||
LanguageChip()
|
||||
}
|
||||
Column(modifier = Modifier
|
||||
.weight(1.25f)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
){
|
||||
examples()
|
||||
val locale = LocalLocale.current
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier = Modifier
|
||||
.size(815.dp, 450.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
chip {
|
||||
Text(
|
||||
text = locale["welcome.action.examples"],
|
||||
)
|
||||
}
|
||||
chip {
|
||||
Text(
|
||||
text = locale["welcome.action.tutorials"],
|
||||
)
|
||||
}
|
||||
intro()
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1.25f)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
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"],
|
||||
)
|
||||
}
|
||||
PDEButton(onClick = { println("Open") }) {
|
||||
val locale = LocalLocale.current
|
||||
Text(locale["welcome.action.go"])
|
||||
}
|
||||
examples()
|
||||
actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,146 +148,82 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
|
||||
}
|
||||
}
|
||||
|
||||
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<Example> {
|
||||
val sketchbook = rememberSketchbookPath()
|
||||
val resources = File(System.getProperty("compose.application.resources.dir") ?: "")
|
||||
var examples by remember { mutableStateOf(emptyList<Example>()) }
|
||||
|
||||
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<Path>()
|
||||
val examples = library.resolve("examples")
|
||||
if (!Files.exists(examples) || !examples.isDirectory()) return@flatMap emptyList()
|
||||
|
||||
Files.walkFileTree(library, object : SimpleFileVisitor<Path>() {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun examples(){
|
||||
val examples = loadExamples()
|
||||
// grab 4 random ones
|
||||
val randoms = examples.shuffled().take(4)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
){
|
||||
items(randoms){ example ->
|
||||
Column(
|
||||
fun actions(){
|
||||
val locale = LocalLocale.current
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
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"],
|
||||
modifier = Modifier
|
||||
.onPointerEvent(PointerEventType.Press) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Release) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Enter) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Exit) {
|
||||
}
|
||||
.pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR)))
|
||||
) {
|
||||
val imageBitmap: ImageBitmap = remember(example.image) {
|
||||
example.image.inputStream().readAllBytes().decodeToImageBitmap()
|
||||
}
|
||||
Image(
|
||||
painter = BitmapPainter(imageBitmap),
|
||||
contentDescription = example.title,
|
||||
modifier = Modifier
|
||||
.background(colors.primary)
|
||||
.width(185.dp)
|
||||
.aspectRatio(16f / 9f)
|
||||
)
|
||||
Text(example.title)
|
||||
}
|
||||
.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"],
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(typography.body1.fontSize.value.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
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"],
|
||||
)
|
||||
}
|
||||
PDEButton(onClick = { println("Open") }) {
|
||||
val locale = LocalLocale.current
|
||||
Text(locale["welcome.action.go"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chip(content: @Composable () -> Unit){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(colors.surface)
|
||||
.border(1.dp, colors.primary, RoundedCornerShape(12.dp))
|
||||
.padding(vertical = 4.dp, horizontal = 12.dp)
|
||||
){
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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.MaterialTheme.typography
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Language
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
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.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
|
||||
)
|
||||
|
||||
@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()
|
||||
val fs = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
|
||||
Files.list(fs.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
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: 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",
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(typography.body1.fontSize.value.dp)
|
||||
)
|
||||
}) {
|
||||
Text(currentLanguage.name)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
){
|
||||
for (language in languages){
|
||||
DropdownMenuItem(onClick = {
|
||||
locale.set(language.locale)
|
||||
expanded = false
|
||||
}) {
|
||||
Text(language.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package processing.app.ui.components.examples
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.MaterialTheme.colors
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
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 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<Example> {
|
||||
val sketchbook = rememberSketchbookPath()
|
||||
val resources = File(System.getProperty("compose.application.resources.dir") ?: "")
|
||||
var examples by remember { mutableStateOf(emptyList<Example>()) }
|
||||
|
||||
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<Path>()
|
||||
val examples = library.resolve("examples")
|
||||
if (!Files.exists(examples) || !examples.isDirectory()) return@flatMap emptyList()
|
||||
|
||||
Files.walkFileTree(library, object : SimpleFileVisitor<Path>() {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun examples(){
|
||||
val examples = loadExamples()
|
||||
// grab 4 random ones
|
||||
val randoms = examples.shuffled().take(4)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
){
|
||||
items(randoms){ example ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.onPointerEvent(PointerEventType.Press) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Release) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Enter) {
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Exit) {
|
||||
}
|
||||
.pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR)))
|
||||
) {
|
||||
val imageBitmap: ImageBitmap = remember(example.image) {
|
||||
example.image.inputStream().readAllBytes().decodeToImageBitmap()
|
||||
}
|
||||
Image(
|
||||
painter = BitmapPainter(imageBitmap),
|
||||
contentDescription = example.title,
|
||||
modifier = Modifier
|
||||
.background(colors.primary)
|
||||
.width(185.dp)
|
||||
.aspectRatio(16f / 9f)
|
||||
)
|
||||
Text(example.title)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package processing.app.ui.theme
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Chip
|
||||
import androidx.compose.material.ChipColors
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.onSurface.copy(alpha = 0.12f)),
|
||||
colors = ChipDefaults.chipColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = colors.onSurface
|
||||
),
|
||||
leadingIcon = leadingIcon,
|
||||
modifier = Modifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package processing.app.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import processing.app.LocalPreferences
|
||||
import processing.app.Messages
|
||||
import processing.app.Platform
|
||||
@@ -12,13 +12,20 @@ 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,8 +35,11 @@ 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<Locale> { error("No Locale Set") }
|
||||
@Composable
|
||||
fun LocaleProvider(content: @Composable () -> Unit) {
|
||||
PlatformStart()
|
||||
@@ -37,9 +47,25 @@ fun LocaleProvider(content: @Composable () -> Unit) {
|
||||
val settingsFolder = Platform.getSettingsFolder()
|
||||
val languageFile = File(settingsFolder, "language.txt")
|
||||
watchFile(languageFile)
|
||||
var code by remember{ mutableStateOf(languageFile.readText().substring(0, 2)) }
|
||||
|
||||
val locale = Locale(languageFile.readText().substring(0, 2))
|
||||
CompositionLocalProvider(LocalLocale provides locale) {
|
||||
content()
|
||||
fun setLocale(locale: java.util.Locale) {
|
||||
java.util.Locale.setDefault(locale)
|
||||
languageFile.writeText(locale.language)
|
||||
code = locale.language
|
||||
}
|
||||
|
||||
|
||||
val locale = Locale(code, ::setLocale)
|
||||
Messages.log("Locale: $code")
|
||||
val dir = when(locale["locale.direction"]) {
|
||||
"rtl" -> LayoutDirection.Rtl
|
||||
else -> LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides dir) {
|
||||
CompositionLocalProvider(LocalLocale provides locale) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user