From 3d81b02ffed2c28560bc2067f1584e05db0bad51 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Sat, 8 Feb 2025 22:47:28 +0100 Subject: [PATCH] Welcome Screen: Language selection --- app/build.gradle.kts | 2 + app/src/processing/app/ui/Welcome.kt | 333 ++++++------------ .../app/ui/components/LanuageSelector.kt | 113 ++++++ .../app/ui/components/examples/Examples.kt | 166 +++++++++ app/src/processing/app/ui/theme/Chip.kt | 39 ++ app/src/processing/app/ui/theme/Locale.kt | 52 ++- 6 files changed, 471 insertions(+), 234 deletions(-) create mode 100644 app/src/processing/app/ui/components/LanuageSelector.kt create mode 100644 app/src/processing/app/ui/components/examples/Examples.kt create mode 100644 app/src/processing/app/ui/theme/Chip.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8dd6e88f0..bfcb65cc0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,6 +21,7 @@ tasks.withType { 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) diff --git a/app/src/processing/app/ui/Welcome.kt b/app/src/processing/app/ui/Welcome.kt index 981eb059c..5173b239c 100644 --- a/app/src/processing/app/ui/Welcome.kt +++ b/app/src/processing/app/ui/Welcome.kt @@ -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 { 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 { - 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) - } - - - @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) { 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 000000000..532e02d66 --- /dev/null +++ b/app/src/processing/app/ui/components/LanuageSelector.kt @@ -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()) + 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) + } + } + } + } +} \ 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 000000000..1c08dc668 --- /dev/null +++ b/app/src/processing/app/ui/components/examples/Examples.kt @@ -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 { + 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) +} + + +@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) + } + } + + } +} + 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 000000000..6d44bc512 --- /dev/null +++ b/app/src/processing/app/ui/theme/Chip.kt @@ -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 + ) +} \ 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 254c0946c..6490c0eb9 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -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 { 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() + } } } \ No newline at end of file