mirror of
https://github.com/processing/processing4.git
synced 2026-01-22 07:51:08 +01:00
Welcome screen (#1353)
* Refactor Locale class and add LocaleProvider test * Make setLocale parameter nullable in Locale class Changed the setLocale parameter in the Locale class to be nullable and updated its usage to safely invoke it. This allows for more flexible instantiation when a setLocale function is not required. * Add compose ui test to the deps * Update locale change method in test Replaces the call to locale.setLocale with locale.set in LocaleKtTest to match the updated API for changing the locale. * Add PDE window utilities for Compose and Swing Introduces PDESwingWindow and PDEComposeWindow classes to simplify creating themed and localized windows in Compose and Swing applications. Includes macOS-specific handling for full window content and localization support for window titles. * Refactor beta welcome window handling Replaces custom JFrame setup in WelcomeToBeta with PDESwingWindow and PDEComposeWindow, centralizing window logic and close handling. Adds onClose callback to PDESwingWindow for improved lifecycle management. Also ensures beta welcome preference is reset on forced update check. * Remove ContributionManager and ContributionPane UI files (#1276) Deleted ContributionManager.kt and ContributionPane.kt from the contrib/ui directory. This removes the Compose-based contributions manager and its detail pane prototypes which got merged unnecessarily * Refactor Locale class and add LocaleProvider test (#1283) * Refactor Locale class and add LocaleProvider test * Make setLocale parameter nullable in Locale class Changed the setLocale parameter in the Locale class to be nullable and updated its usage to safely invoke it. This allows for more flexible instantiation when a setLocale function is not required. * Add compose ui test to the deps * Update locale change method in test Replaces the call to locale.setLocale with locale.set in LocaleKtTest to match the updated API for changing the locale. * Theming (#1298) * Add Material3-based Processing theme and typography Introduces Colors.kt with custom color schemes for light and dark themes using Material3. Refactors Theme.kt to use Material3 theming, adds a PDETheme composable, and provides a desktop preview app for theme components. Updates Typography.kt to use Space Grotesk font family and defines new typography styles for Material3. * Refactor to use Material3 and update theme usage Replaces Material2 components with Material3 in WelcomeToBeta, removes custom PDEButton in favor of Material3 Button, and updates theme usage to PDETheme. Also simplifies background modifier in PDETheme and removes unused Kotlin Multiplatform plugin from build.gradle.kts. * Add Space Grotesk font files and license Includes SpaceGrotesk font variants (Bold, Light, Medium, Regular, SemiBold) and the associated SIL Open Font License. This enables usage of the Space Grotesk typeface in the project. * Update markdown renderer to m3 and adjust UI Switched markdown renderer imports from m2 to m3 and updated the dependency version to 0.37.0. Adjusted WelcomeToBeta window size, layout, and logo dimensions for improved appearance. Ensured Box in Theme.kt fills available space for better layout consistency. * Switch from ProcessingTheme to PDETheme in window UI Replaces the use of ProcessingTheme with PDETheme in the PDEWindowContent composable * Refactor preferences to Jetpack Compose UI Replaces the legacy PreferencesFrame with a new Jetpack Compose-based preferences UI. Adds reactive preferences management using a custom ReactiveProperties class, and introduces modular preference groups (General, Interface, Other) with composable controls. Updates Base.java to launch the new preferences window, and refactors theme and window code for Compose integration. * Remove obsolete TODO for onClose callback * Clean up handlePrefs method by removing comments Removed commented-out code for preferences frame initialization. * Composable Preferences rewrite (#1277) * Remove ContributionManager and ContributionPane UI files Deleted ContributionManager.kt and ContributionPane.kt from the contrib/ui directory. This removes the Compose-based contributions manager and its detail pane prototypes which got merged unnecessarily * Enhance Preferences reactivity and test coverage Refactored ReactiveProperties to use snapshotStateMap for Compose reactivity. Improved PreferencesProvider and watchFile composables with better file watching, override support via system properties, and added documentation. Updated PreferencesKtTest to use temporary files and verify file-to-UI reactivity. * Small bugfix for removed function * Add compose ui test to the deps * Welcome screen implementation (#1307) * Remove ContributionManager and ContributionPane UI files Deleted ContributionManager.kt and ContributionPane.kt from the contrib/ui directory. This removes the Compose-based contributions manager and its detail pane prototypes which got merged unnecessarily * Enhance Preferences reactivity and test coverage Refactored ReactiveProperties to use snapshotStateMap for Compose reactivity. Improved PreferencesProvider and watchFile composables with better file watching, override support via system properties, and added documentation. Updated PreferencesKtTest to use temporary files and verify file-to-UI reactivity. * Small bugfix for removed function * Add compose ui test to the deps * Refactor theme system to Material 3 color schemes Replaces legacy color definitions with Material 3 color schemes and introduces extended color support for warnings. Dialogs in Messages.kt are now implemented using Compose Material 3 components for a modern UI. Removes deprecated color sets and updates PDETheme to use new color schemes, improving consistency and maintainability. * Add PDEWelcome Composable UI screen Introduces a new PDEWelcome.kt file with a Composable UI for the Processing welcome screen. Includes layout with buttons for language selection, new sketch, examples, and sketchbook, as well as a placeholder for right-side content and a main entry point for launching the window. * Initial layout * Revamp welcome screen UI and add social icons Refactors the PDEWelcome screen to improve layout, update button icons, and add support for Discord, GitHub, and Instagram SVG icons. The welcome screen now receives a Base instance for proper action handling, and new methods replace deprecated ones in Base.java. Updates related menu actions to pass the Base instance as needed. * Add example previews to welcome screen Replaces placeholder text on the right side of the PDEWelcome screen with a LazyColumn displaying example sketches. Each example attempts to show a preview image if available, or a placeholder icon otherwise. Introduces an Example data class and related image loading logic. * Add hover-activated play button to example previews Introduced a hover effect on example preview images in the welcome screen, displaying a play button that opens the example when clicked. Refactored title key usage for consistency. * Localize welcome screen UI strings Replaced hardcoded strings in the PDEWelcome screen with localized values using the LocalLocale context. Added new keys for the welcome screen to the English and Dutch language property files to support internationalization. * Add language selector and UI improvements to welcome screen Introduces a language selection dropdown to the PDE welcome screen using a shared composable from preferences. Refactors the layout for better spacing, updates example cards with animated overlays, and replaces the show-on-startup button with a checkbox. Also adds a new translation key for the open example button. * Refactor example listing and randomize welcome sketches Moved example folder listing logic in Contributions.ExamplesList to a companion object function for reuse. Updated PDEWelcome to display a randomized selection of sketches from all available examples, replacing the previous static list. * Refactor example handling to use Sketch objects Replaces Example objects with Sketch objects for managing example sketches in the welcome screen. Updates all relevant usages to reference Sketch properties, simplifying the code and improving clarity. * Add vertical scrollbar to welcome screen examples Introduces a VerticalScrollbar to the examples list in the PDEWelcome screen for improved navigation. Also adjusts spacing and arrangement in several UI components for better layout consistency, and updates the welcome screen title in the language properties. * Add rounded corners to buttons in PDEWelcome Introduced a RoundedCornerShape with 12.dp radius and applied it to various buttons in the PDEWelcome screen for improved UI consistency and aesthetics. * Refactor PDEWelcome UI and add Sketch card composable Refactored the PDEWelcome screen for improved structure and readability, including extracting the example preview into a reusable Sketch.card composable. Updated icon usage for RTL support, adjusted layout and padding, and improved the examples list initialization. Also, customized scrollbar style in PDETheme for a more consistent UI appearance. * Add unique window handling to prevent duplicates Introduces a 'unique' parameter to PDESwingWindow and PDEComposeWindow, allowing windows to be identified by a KClass and preventing multiple instances of the same window. If a window with the same unique identifier exists, it is brought to the front and the new one is disposed. This helps avoid duplicate welcome or other singleton windows. * Refactor dialog handling and improve AlertDialog UI Refactored the showDialog function to accept a modifier and updated all AlertDialog usages to use RectangleShape and the modifier parameter. Improved dialog sizing and positioning by dynamically adjusting the window size based on content, and set additional window properties for better integration on macOS. * Set application window icon using Toolkit.setIcon Added calls to Toolkit.setIcon(window) in Start.kt and Window.kt to ensure the application window icon is set consistent * Simplify imports and update scrollbar colors in Theme.kt Consolidated import statements for Compose libraries using wildcard imports to reduce verbosity. Updated scrollbar hover and unhover colors to use the default outlineVariant color without alpha modification. * Removing the Preferences work to keep the PR clean * Update background color in PDEWelcome UI Changed the background color from surfaceContainerLow to surfaceContainerLowest in the PDEWelcome composable for improved visual consistency with the MaterialTheme. * Tweak welcome actions naming and order - Rename `Empty Sketch` to `New Sketch` - Rename `Sketchbook` to `My Sketches` - Move `Open Examples` below `My Sketches` * Rather than setting the decorations app wide, just modify the editor screen --------- Co-authored-by: Raphaël de Courville <groupes.raphael@gmail.com> * Replace ProcessingTheme with PDETheme in WelcomeSurvey * Add Material Theme Builder file headers Added autogenerated file headers to Color.kt and Theme.kt indicating they were generated by the Material Theme Builder tool and should not be edited directly. Also reordered imports in Theme.kt for consistency. * Fix preferences file override and update test property Corrects the logic for selecting the preferences file in PreferencesProvider to use the override if present. Updates the test to set the correct system property for the settings folder. * Added survey button to the new welcome screen --------- Co-authored-by: Raphaël de Courville <groupes.raphael@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
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
|
||||
@@ -16,6 +17,7 @@ plugins{
|
||||
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.jetbrainsCompose)
|
||||
|
||||
alias(libs.plugins.serialization)
|
||||
alias(libs.plugins.download)
|
||||
}
|
||||
@@ -59,7 +61,7 @@ compose.desktop {
|
||||
).map { "-D${it.first}=${it.second}" }.toTypedArray())
|
||||
|
||||
nativeDistributions{
|
||||
modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting", "jdk.httpserver")
|
||||
modules("jdk.jdi", "java.compiler", "jdk.accessibility", "jdk.zipfs", "java.management.rmi", "java.scripting", "jdk.httpserver")
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "Processing"
|
||||
|
||||
@@ -107,25 +109,29 @@ dependencies {
|
||||
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.material3)
|
||||
|
||||
implementation(libs.compottie)
|
||||
implementation(libs.kaml)
|
||||
implementation(libs.markdown)
|
||||
implementation(libs.markdownJVM)
|
||||
|
||||
implementation(libs.clikt)
|
||||
implementation(libs.kotlinxSerializationJson)
|
||||
|
||||
@OptIn(ExperimentalComposeLibrary::class)
|
||||
testImplementation(compose.uiTest)
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(libs.mockitoKotlin)
|
||||
testImplementation(libs.junitJupiter)
|
||||
testImplementation(libs.junitJupiterParams)
|
||||
|
||||
implementation(libs.clikt)
|
||||
implementation(libs.kotlinxSerializationJson)
|
||||
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
3
app/src/main/resources/icons/Discord.svg
Normal file
3
app/src/main/resources/icons/Discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="15" viewBox="0 0 20 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.1368 1.20528C14.9009 0.637704 13.5966 0.232841 12.2566 0.000827533C12.2444 -0.001217 12.232 0.000548795 12.2209 0.00588217C12.2098 0.0112156 12.2006 0.0198546 12.1946 0.0306113C12.0267 0.328449 11.8408 0.718021 11.7109 1.02301C10.2665 0.80399 8.79738 0.80399 7.35298 1.02301C7.20874 0.682977 7.04485 0.351619 6.86214 0.0306113C6.85616 0.0198546 6.84699 0.0112156 6.8359 0.00588217C6.8248 0.000548795 6.81233 -0.001217 6.80019 0.000827533C5.45992 0.23195 4.15539 0.637009 2.91996 1.20528C2.90934 1.20923 2.90047 1.21683 2.89494 1.22673C0.424078 4.91873 -0.253801 8.52018 0.0785863 12.0764C0.080969 12.0938 0.0893085 12.1085 0.103605 12.1204C1.54272 13.1858 3.15199 13.9995 4.86305 14.527C4.87514 14.5308 4.88812 14.5307 4.90012 14.5266C4.91212 14.5225 4.9225 14.5147 4.92977 14.5043C5.2975 14.004 5.62234 13.4762 5.9043 12.921C5.91016 12.9096 5.91216 12.8966 5.91002 12.8839C5.90787 12.8712 5.90168 12.8596 5.89238 12.8507L5.87094 12.8376C5.35789 12.6398 4.86083 12.4028 4.38413 12.1288C4.37082 12.1212 4.36093 12.1089 4.35649 12.0942C4.35206 12.0796 4.35342 12.0638 4.3603 12.0502L4.37817 12.0275C4.47904 11.9521 4.57753 11.8746 4.67363 11.7952C4.68205 11.7884 4.69215 11.7841 4.70285 11.7826C4.71356 11.7812 4.72446 11.7826 4.73439 11.7869C7.85454 13.2117 11.232 13.2117 14.314 11.7869C14.3243 11.7821 14.3356 11.7804 14.3468 11.7819C14.358 11.7834 14.3685 11.788 14.3772 11.7952C14.4733 11.8746 14.5718 11.9521 14.6726 12.0275C14.681 12.0333 14.6878 12.041 14.6923 12.0501C14.6969 12.0592 14.699 12.0693 14.6985 12.0795C14.698 12.0896 14.695 12.0995 14.6896 12.1081C14.6842 12.1167 14.6768 12.1239 14.6679 12.1288C14.1932 12.4072 13.6954 12.644 13.1799 12.8365C13.1717 12.8394 13.1642 12.8442 13.158 12.8503C13.1519 12.8565 13.1471 12.864 13.1441 12.8722C13.1412 12.8801 13.1401 12.8886 13.1407 12.897C13.1413 12.9054 13.1437 12.9136 13.1477 12.921C13.4336 13.475 13.7613 14.004 14.121 14.5043C14.1283 14.5147 14.1387 14.5225 14.1507 14.5266C14.1627 14.5307 14.1757 14.5308 14.1878 14.527C15.9022 14.0024 17.5143 13.1885 18.9544 12.1204C18.9615 12.1154 18.9675 12.1088 18.9718 12.1012C18.9761 12.0936 18.9787 12.0851 18.9794 12.0764C19.3773 7.96501 18.3134 4.39334 16.1606 1.22792C16.1588 1.22252 16.1556 1.21764 16.1515 1.2137C16.1473 1.20976 16.1423 1.20688 16.1368 1.20528ZM6.37011 9.91049C5.43014 9.91049 4.65695 9.04795 4.65695 7.99003C4.65695 6.93211 5.41584 6.06838 6.37011 6.06838C7.33154 6.06838 8.09758 6.93807 8.08328 7.99003C8.08328 9.04795 7.32439 9.91049 6.37011 9.91049ZM12.7033 9.91049C11.7646 9.91049 10.9902 9.04795 10.9902 7.99003C10.9902 6.93211 11.7491 6.06838 12.7033 6.06838C13.6648 6.06838 14.432 6.93807 14.4165 7.99003C14.4165 9.04795 13.6648 9.91049 12.7033 9.91049Z" fill="#6D6D6D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
10
app/src/main/resources/icons/GitHub.svg
Normal file
10
app/src/main/resources/icons/GitHub.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_62831_204)">
|
||||
<path d="M9.53082 0C14.7966 0 19.0616 4.26504 19.0616 9.53081C19.0611 11.5278 18.4344 13.4743 17.2696 15.0964C16.1049 16.7184 14.4608 17.9344 12.5688 18.5732C12.0922 18.6685 11.9135 18.3706 11.9135 18.1205C11.9135 17.7988 11.9254 16.7742 11.9254 15.4995C11.9254 14.606 11.6276 14.0341 11.2821 13.7363C13.4027 13.498 15.6305 12.6879 15.6305 9.03045C15.6305 7.98206 15.2612 7.1362 14.6536 6.46904C14.7489 6.23077 15.0825 5.25386 14.5583 3.94337C14.5583 3.94337 13.7601 3.68128 11.9373 4.92028C11.1749 4.70584 10.3648 4.59862 9.55464 4.59862C8.74452 4.59862 7.9344 4.70584 7.17194 4.92028C5.34917 3.69319 4.55097 3.94337 4.55097 3.94337C4.02677 5.25386 4.36035 6.23077 4.45566 6.46904C3.84807 7.1362 3.47875 7.99397 3.47875 9.03045C3.47875 12.676 5.69466 13.498 7.81527 13.7363C7.54126 13.9746 7.29107 14.3915 7.20768 15.011C6.65966 15.2612 5.2896 15.6663 4.43183 14.2247C4.25313 13.9388 3.71702 13.2359 2.96647 13.2478C2.16826 13.2597 2.6448 13.7005 2.97838 13.8792C3.38344 14.1056 3.84807 14.9515 3.95529 15.2255C4.14591 15.7616 4.76541 16.7861 7.16003 16.3453C7.16003 17.1436 7.17194 17.8941 7.17194 18.1205C7.17194 18.3706 6.99324 18.6566 6.5167 18.5732C4.61844 17.9413 2.96734 16.7278 1.79762 15.1047C0.627898 13.4817 -0.00104896 11.5315 1.31324e-06 9.53081C1.31324e-06 4.26504 4.26504 0 9.53082 0Z" fill="#6D6D6D"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_62831_204">
|
||||
<rect width="19.0616" height="19.0616" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
app/src/main/resources/icons/Instagram.svg
Normal file
3
app/src/main/resources/icons/Instagram.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.53081 0C6.94439 0 6.61915 0.0119135 5.60293 0.0571849C4.5867 0.104839 3.89453 0.26448 3.28813 0.500368C2.65176 0.739026 2.0755 1.11439 1.59999 1.59999C1.11439 2.0755 0.739026 2.65176 0.500368 3.28813C0.26448 3.89334 0.103648 4.5867 0.0571849 5.59935C0.0119135 6.61796 0 6.94201 0 9.53201C0 12.1196 0.0119135 12.4437 0.0571849 13.4599C0.104839 14.4749 0.26448 15.1671 0.500368 15.7735C0.744595 16.4001 1.06983 16.9315 1.59999 17.4616C2.12895 17.9918 2.66029 18.3182 3.28694 18.5613C3.89453 18.7971 4.58551 18.958 5.60054 19.0044C6.61796 19.0497 6.94201 19.0616 9.53081 19.0616C12.1196 19.0616 12.4425 19.0497 13.4599 19.0044C14.4737 18.9568 15.1683 18.7971 15.7747 18.5613C16.4106 18.3224 16.9865 17.9471 17.4616 17.4616C17.9918 16.9315 18.317 16.4001 18.5613 15.7735C18.796 15.1671 18.9568 14.4749 19.0044 13.4599C19.0497 12.4437 19.0616 12.1196 19.0616 9.53081C19.0616 6.94201 19.0497 6.61796 19.0044 5.60054C18.9568 4.5867 18.796 3.89334 18.5613 3.28813C18.3226 2.65176 17.9472 2.0755 17.4616 1.59999C16.9861 1.11439 16.4099 0.739026 15.7735 0.500368C15.1659 0.26448 14.4725 0.103648 13.4587 0.0571849C12.4413 0.0119135 12.1184 0 9.52843 0H9.53081ZM8.67661 1.71793H9.53201C12.0767 1.71793 12.3781 1.72627 13.3825 1.77273C14.3117 1.81443 14.8168 1.9705 15.1528 2.10035C15.5972 2.2731 15.9153 2.48039 16.2488 2.81397C16.5824 3.14755 16.7885 3.46445 16.9613 3.91002C17.0923 4.24479 17.2472 4.74992 17.2889 5.67917C17.3354 6.68348 17.3449 6.9849 17.3449 9.52843C17.3449 12.072 17.3354 12.3746 17.2889 13.3789C17.2472 14.3081 17.0911 14.8121 16.9613 15.148C16.8074 15.5613 16.5637 15.9353 16.2477 16.2429C15.9141 16.5765 15.5972 16.7826 15.1516 16.9553C14.818 17.0864 14.3129 17.2412 13.3825 17.2841C12.3781 17.3294 12.0767 17.3401 9.53201 17.3401C6.98728 17.3401 6.68467 17.3294 5.68037 17.2841C4.75111 17.2412 4.24717 17.0864 3.91121 16.9553C3.49761 16.8019 3.12325 16.5586 2.81516 16.2429C2.49859 15.935 2.25443 15.5607 2.10035 15.1468C1.9705 14.8121 1.81443 14.3069 1.77273 13.3777C1.72746 12.3734 1.71793 12.072 1.71793 9.52605C1.71793 6.98013 1.72746 6.6811 1.77273 5.67679C1.81562 4.74754 1.9705 4.2424 2.10154 3.90644C2.27429 3.46207 2.48159 3.14398 2.81516 2.8104C3.14874 2.47682 3.46564 2.27072 3.91121 2.09797C4.24717 1.96692 4.75111 1.81205 5.68037 1.76916C6.55958 1.72865 6.90031 1.71674 8.67661 1.71555V1.71793ZM14.6191 3.30004C14.4689 3.30004 14.3202 3.32963 14.1814 3.3871C14.0426 3.44458 13.9166 3.52882 13.8104 3.63503C13.7042 3.74123 13.6199 3.86731 13.5624 4.00607C13.505 4.14483 13.4754 4.29355 13.4754 4.44374C13.4754 4.59393 13.505 4.74266 13.5624 4.88142C13.6199 5.02018 13.7042 5.14626 13.8104 5.25246C13.9166 5.35866 14.0426 5.4429 14.1814 5.50038C14.3202 5.55786 14.4689 5.58744 14.6191 5.58744C14.9224 5.58744 15.2133 5.46694 15.4278 5.25246C15.6423 5.03797 15.7628 4.74707 15.7628 4.44374C15.7628 4.14041 15.6423 3.84951 15.4278 3.63503C15.2133 3.42054 14.9224 3.30004 14.6191 3.30004ZM9.53201 4.63674C8.8828 4.62661 8.23807 4.74573 7.63535 4.98717C7.03262 5.2286 6.48395 5.58752 6.02127 6.04304C5.55859 6.49856 5.19115 7.04156 4.94035 7.64045C4.68954 8.23933 4.56038 8.88213 4.56038 9.53141C4.56038 10.1807 4.68954 10.8235 4.94035 11.4224C5.19115 12.0213 5.55859 12.5643 6.02127 13.0198C6.48395 13.4753 7.03262 13.8342 7.63535 14.0757C8.23807 14.3171 8.8828 14.4362 9.53201 14.4261C10.8169 14.406 12.0424 13.8815 12.944 12.9658C13.8456 12.05 14.3509 10.8165 14.3509 9.53141C14.3509 8.24633 13.8456 7.01279 12.944 6.09704C12.0424 5.18129 10.8169 4.65679 9.53201 4.63674ZM9.53201 6.35348C9.94926 6.35348 10.3624 6.43566 10.7479 6.59534C11.1334 6.75501 11.4837 6.98906 11.7787 7.2841C12.0738 7.57914 12.3078 7.92941 12.4675 8.3149C12.6272 8.70039 12.7093 9.11356 12.7093 9.53081C12.7093 9.94807 12.6272 10.3612 12.4675 10.7467C12.3078 11.1322 12.0738 11.4825 11.7787 11.7775C11.4837 12.0726 11.1334 12.3066 10.7479 12.4663C10.3624 12.626 9.94926 12.7081 9.53201 12.7081C8.68932 12.7081 7.88116 12.3734 7.28529 11.7775C6.68942 11.1817 6.35467 10.3735 6.35467 9.53081C6.35467 8.68813 6.68942 7.87996 7.28529 7.2841C7.88116 6.68823 8.68932 6.35348 9.53201 6.35348Z" fill="#6D6D6D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -643,6 +643,23 @@ welcome.survey.description=Processing is free, open-source, and shaped by its co
|
||||
color_chooser = Color Selector
|
||||
color_chooser.select = Select
|
||||
|
||||
|
||||
# ---------------------------------------
|
||||
# Welcome Screen
|
||||
welcome.processing.logo = Processing Logo
|
||||
welcome.processing.title = Welcome to Processing
|
||||
welcome.actions.sketch.new = New Sketch
|
||||
welcome.actions.examples = Open Examples
|
||||
welcome.actions.sketchbook = My Sketches
|
||||
welcome.actions.show_startup = Show this window at startup
|
||||
welcome.resources.title = Resources
|
||||
welcome.resources.get_started = Get Started
|
||||
welcome.resources.tutorials = Tutorials
|
||||
welcome.resources.documentation = Reference
|
||||
welcome.community.title = Join our community
|
||||
welcome.community.forum = Forum
|
||||
welcome.sketch.open = Open
|
||||
|
||||
# ---------------------------------------
|
||||
# Movie Maker
|
||||
|
||||
|
||||
@@ -327,6 +327,22 @@ beta.title = Dankuwel voor het testen van deze Processing Beta!
|
||||
beta.message = Deze preview release laat ons feedback verzamelen en problemen oplossen. **Sommige functies werken mogelijk niet zoals verwacht.** Als u problemen ondervindt, [post dan op het forum](https://discourse.processing.org) of [open een GitHub issue](https://github.com/processing/processing4/issues).
|
||||
beta.button = Ok
|
||||
|
||||
|
||||
# ---------------------------------------
|
||||
# Welcome Screen
|
||||
welcome.processing.logo = Processing Logo
|
||||
welcome.processing.title = Welkom bij Processing!
|
||||
welcome.actions.sketch.new = Nieuwe Schets
|
||||
welcome.actions.examples = Open Voorbeelden
|
||||
welcome.actions.show_startup = Laat dit scherm zien bij opstarten
|
||||
welcome.resources.title = Resources
|
||||
welcome.resources.video = Video Cursus
|
||||
welcome.resources.get_started = Om te beginnen
|
||||
welcome.resources.tutorials = Tutorials
|
||||
welcome.resources.documentation = Handleiding
|
||||
welcome.community.title = Neem deel aan de Community
|
||||
welcome.community.forum = Forum
|
||||
|
||||
# ---------------------------------------
|
||||
# Color Chooser
|
||||
color_chooser = Kies een kleur...
|
||||
|
||||
@@ -328,13 +328,7 @@ public class Base {
|
||||
// Needs to be shown after the first editor window opens, so that it
|
||||
// shows up on top, and doesn't prevent an editor window from opening.
|
||||
if (Preferences.getBoolean("welcome.four.show")) {
|
||||
try {
|
||||
new Welcome(base);
|
||||
} catch (IOException e) {
|
||||
Messages.showTrace("Unwelcoming",
|
||||
"Please report this error to\n" +
|
||||
"https://github.com/processing/processing4/issues", e, false);
|
||||
}
|
||||
PDEWelcomeKt.showWelcomeScreen(base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,7 +592,7 @@ public class Base {
|
||||
defaultFileMenu.add(item);
|
||||
|
||||
item = Toolkit.newJMenuItemShift(Language.text("menu.file.examples"), 'O');
|
||||
item.addActionListener(e -> thinkDifferentExamples());
|
||||
item.addActionListener(e -> showExamplesFrame());
|
||||
defaultFileMenu.add(item);
|
||||
|
||||
return defaultFileMenu;
|
||||
@@ -1874,7 +1868,7 @@ public class Base {
|
||||
// }
|
||||
|
||||
|
||||
public void thinkDifferentExamples() {
|
||||
public void showExamplesFrame() {
|
||||
nextMode.showExamplesFrame();
|
||||
}
|
||||
|
||||
@@ -2180,10 +2174,10 @@ public class Base {
|
||||
* Show the Preferences window.
|
||||
*/
|
||||
public void handlePrefs() {
|
||||
if (preferencesFrame == null) {
|
||||
preferencesFrame = new PreferencesFrame(this);
|
||||
}
|
||||
preferencesFrame.showFrame();
|
||||
if (preferencesFrame == null) {
|
||||
preferencesFrame = new PreferencesFrame(this);
|
||||
}
|
||||
preferencesFrame.showFrame();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,13 +18,27 @@
|
||||
*/
|
||||
package processing.app
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import com.formdev.flatlaf.FlatLightLaf
|
||||
import processing.app.ui.Toolkit
|
||||
import processing.app.ui.theme.PDETheme
|
||||
import java.awt.EventQueue
|
||||
import java.awt.Frame
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
|
||||
|
||||
class Messages {
|
||||
companion object {
|
||||
@@ -270,6 +284,37 @@ class Messages {
|
||||
}
|
||||
}
|
||||
}
|
||||
fun main(){
|
||||
val types = mapOf(
|
||||
"message" to { Messages.showMessage("Test Title", "This is a test message.") },
|
||||
"warning" to { Messages.showWarning("Test Warning", "This is a test warning.", Exception("dfdsfjk")) },
|
||||
"trace" to { Messages.showTrace("Test Trace", "This is a test trace.", Exception("Test Exception"), false) },
|
||||
"tiered_warning" to { Messages.showWarningTiered("Test Tiered Warning", "Primary message", "Secondary message", null) },
|
||||
"yes_no" to { Messages.showYesNoQuestion(null, "Test Yes/No", "Do you want to continue?", "Choose yes or no.") },
|
||||
"custom_question" to { Messages.showCustomQuestion(null, "Test Custom Question", "Choose an option:", "Select one of the options below.", 1, "Option 1", "Option 2", "Option 3") },
|
||||
"error" to { Messages.showError("Test Error", "This is a test error.", null) },
|
||||
)
|
||||
Platform.init()
|
||||
UIManager.setLookAndFeel(FlatLightLaf())
|
||||
application {
|
||||
val state = rememberWindowState(
|
||||
size = DpSize(500.dp, 300.dp)
|
||||
)
|
||||
Window(state = state, onCloseRequest = ::exitApplication, title = "Test Messages") {
|
||||
PDETheme {
|
||||
Column {
|
||||
for ((type, action) in types) {
|
||||
Button(onClick = { action() }, modifier = Modifier.padding(8.dp)) {
|
||||
Text("Show $type dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper functions to give the base classes a color
|
||||
fun String.formatClassName() = this
|
||||
|
||||
@@ -138,6 +138,14 @@ public class Preferences {
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether Preferences.init() has been called. If not, we are probably not running the full application.
|
||||
* @return true if Preferences has been initialized
|
||||
*/
|
||||
static public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
|
||||
static void handleProxy(String protocol, String hostProp, String portProp) {
|
||||
String proxyHost = get("proxy." + protocol + ".host");
|
||||
|
||||
@@ -2,59 +2,183 @@ 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 processing.utils.Settings
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardWatchEventKinds
|
||||
import java.nio.file.WatchEvent
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
The ReactiveProperties class extends the standard Java Properties class
|
||||
to provide reactive capabilities using Jetpack Compose's mutableStateMapOf.
|
||||
This allows UI components to automatically update when preference values change.
|
||||
*/
|
||||
class ReactiveProperties : Properties() {
|
||||
val snapshotStateMap = mutableStateMapOf<String, String>()
|
||||
|
||||
override fun setProperty(key: String, value: String) {
|
||||
super.setProperty(key, value)
|
||||
snapshotStateMap[key] = value
|
||||
}
|
||||
|
||||
override fun getProperty(key: String): String? {
|
||||
return snapshotStateMap[key] ?: super.getProperty(key)
|
||||
}
|
||||
|
||||
operator fun get(key: String): String? = getProperty(key)
|
||||
|
||||
operator fun set(key: String, value: String) {
|
||||
setProperty(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A CompositionLocal to provide access to the ReactiveProperties instance
|
||||
throughout the composable hierarchy.
|
||||
*/
|
||||
val LocalPreferences = compositionLocalOf<ReactiveProperties> { error("No preferences provided") }
|
||||
|
||||
const val PREFERENCES_FILE_NAME = "preferences.txt"
|
||||
const val DEFAULTS_FILE_NAME = "defaults.txt"
|
||||
|
||||
fun PlatformStart(){
|
||||
Platform.inst ?: Platform.init()
|
||||
}
|
||||
/*
|
||||
This composable function sets up a preferences provider that manages application settings.
|
||||
It initializes the preferences from a file, watches for changes to that file, and saves
|
||||
any updates back to the file. It uses a ReactiveProperties class to allow for reactive
|
||||
updates in the UI when preferences change.
|
||||
|
||||
usage:
|
||||
PreferencesProvider {
|
||||
// Your app content here
|
||||
}
|
||||
|
||||
to access preferences:
|
||||
val preferences = LocalPreferences.current
|
||||
val someSetting = preferences["someKey"] ?: "defaultValue"
|
||||
preferences["someKey"] = "newValue"
|
||||
|
||||
This will automatically save to the preferences file and update any UI components
|
||||
that are observing that key.
|
||||
|
||||
to override the preferences file (for testing, etc)
|
||||
System.setProperty("processing.app.preferences.file", "/path/to/your/preferences.txt")
|
||||
to override the debounce time (in milliseconds)
|
||||
System.setProperty("processing.app.preferences.debounce", "200")
|
||||
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun loadPreferences(): Properties{
|
||||
PlatformStart()
|
||||
fun PreferencesProvider(content: @Composable () -> Unit) {
|
||||
val preferencesFileOverride: File? = System.getProperty("processing.app.preferences.file")?.let { File(it) }
|
||||
val preferencesDebounceOverride: Long? = System.getProperty("processing.app.preferences.debounce")?.toLongOrNull()
|
||||
|
||||
val settingsFolder = Settings.getFolder()
|
||||
val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME)
|
||||
val preferencesFile = preferencesFileOverride ?: settingsFolder.resolve(PREFERENCES_FILE_NAME)
|
||||
|
||||
if(!preferencesFile.exists()){
|
||||
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 {
|
||||
val defaultsStream = ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME)
|
||||
?: InputStream.nullInputStream()
|
||||
load(
|
||||
defaultsStream
|
||||
.reader(Charsets.UTF_8)
|
||||
)
|
||||
load(
|
||||
preferencesFile
|
||||
.inputStream()
|
||||
.reader(Charsets.UTF_8)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val initialState = remember(properties) { properties.snapshotStateMap.toMap() }
|
||||
|
||||
// Listen for changes to the preferences and save them to file
|
||||
LaunchedEffect(properties) {
|
||||
snapshotFlow { properties.snapshotStateMap.toMap() }
|
||||
.dropWhile { it == initialState }
|
||||
.debounce(preferencesDebounceOverride ?: 100)
|
||||
.collect {
|
||||
|
||||
// Save the preferences to file, sorted alphabetically
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
This composable function watches a specified file for modifications. When the file is modified,
|
||||
it updates a state variable with the latest WatchEvent. This can be useful for triggering UI updates
|
||||
or other actions in response to changes in the file.
|
||||
|
||||
To watch the file at the fasted speed (for testing) set the following system property:
|
||||
System.setProperty("processing.app.watchfile.forced", "true")
|
||||
*/
|
||||
@Composable
|
||||
fun watchFile(file: File): Any? {
|
||||
val scope = rememberCoroutineScope()
|
||||
var event by remember(file) { mutableStateOf<WatchEvent<*>?> (null) }
|
||||
val forcedWatch: Boolean = System.getProperty("processing.app.watchfile.forced").toBoolean()
|
||||
|
||||
DisposableEffect(file){
|
||||
val scope = rememberCoroutineScope()
|
||||
var event by remember(file) { mutableStateOf<WatchEvent<*>?>(null) }
|
||||
|
||||
DisposableEffect(file) {
|
||||
val fileSystem = FileSystems.getDefault()
|
||||
val watcher = fileSystem.newWatchService()
|
||||
|
||||
var active = true
|
||||
|
||||
// In forced mode we just poll the last modified time of the file
|
||||
// This is not efficient but works better for testing with temp files
|
||||
val toWatch = { file.lastModified() }
|
||||
var state = toWatch()
|
||||
|
||||
val path = file.toPath()
|
||||
val parent = path.parent
|
||||
val key = parent.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (active) {
|
||||
for (modified in key.pollEvents()) {
|
||||
if (modified.context() != path.fileName) continue
|
||||
event = modified
|
||||
if (forcedWatch) {
|
||||
if (toWatch() == state) continue
|
||||
state = toWatch()
|
||||
event = object : WatchEvent<Path> {
|
||||
override fun count(): Int = 1
|
||||
override fun context(): Path = file.toPath().fileName
|
||||
override fun kind(): WatchEvent.Kind<Path> = StandardWatchEventKinds.ENTRY_MODIFY
|
||||
override fun toString(): String = "ForcedEvent(${context()})"
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
for (modified in key.pollEvents()) {
|
||||
if (modified.context() != path.fileName) continue
|
||||
event = modified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,12 +189,4 @@ fun watchFile(file: File): Any? {
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
val LocalPreferences = compositionLocalOf<Properties> { error("No preferences provided") }
|
||||
@Composable
|
||||
fun PreferencesProvider(content: @Composable () -> Unit){
|
||||
val preferences = loadPreferences()
|
||||
CompositionLocalProvider(LocalPreferences provides preferences){
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@ class Contributions: SuspendingCliktCommand(){
|
||||
}
|
||||
|
||||
class ExamplesList: SuspendingCliktCommand("list") {
|
||||
|
||||
|
||||
val serializer = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
@@ -37,107 +35,121 @@ class Contributions: SuspendingCliktCommand(){
|
||||
override fun help(context: Context) = "List all examples"
|
||||
override suspend fun run() {
|
||||
Platform.init()
|
||||
// TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now
|
||||
// TODO: Allow the user to change the sketchbook location
|
||||
// TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode
|
||||
val sketchbookFolder = Platform.getDefaultSketchbookFolder()
|
||||
val resourcesDir = System.getProperty("compose.application.resources.dir")
|
||||
|
||||
val javaMode = "$resourcesDir/modes/java"
|
||||
|
||||
val javaModeExamples = File("$javaMode/examples")
|
||||
.listFiles()
|
||||
?.map { getSketches(it)}
|
||||
?: emptyList()
|
||||
|
||||
val javaModeLibrariesExamples = File("$javaMode/libraries")
|
||||
.listFiles{ it.isDirectory }
|
||||
?.map { library ->
|
||||
val properties = library.resolve("library.properties")
|
||||
val name = findNameInProperties(properties) ?: library.name
|
||||
|
||||
val libraryExamples = getSketches(library.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = name,
|
||||
path = library.absolutePath,
|
||||
mode = "java",
|
||||
children = libraryExamples?.children ?: emptyList(),
|
||||
sketches = libraryExamples?.sketches ?: emptyList()
|
||||
)
|
||||
} ?: emptyList()
|
||||
val javaModeLibraries = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Libraries",
|
||||
path = "$javaMode/libraries",
|
||||
mode = "java",
|
||||
children = javaModeLibrariesExamples,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
val contributedLibraries = sketchbookFolder.resolve("libraries")
|
||||
.listFiles{ it.isDirectory }
|
||||
?.map { library ->
|
||||
val properties = library.resolve("library.properties")
|
||||
val name = findNameInProperties(properties) ?: library.name
|
||||
// Get library name from library.properties if it exists
|
||||
val libraryExamples = getSketches(library.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = name,
|
||||
path = library.absolutePath,
|
||||
mode = "java",
|
||||
children = libraryExamples?.children ?: emptyList(),
|
||||
sketches = libraryExamples?.sketches ?: emptyList()
|
||||
)
|
||||
} ?: emptyList()
|
||||
|
||||
val contributedLibrariesFolder = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Contributed Libraries",
|
||||
path = sketchbookFolder.resolve("libraries").absolutePath,
|
||||
mode = "java",
|
||||
children = contributedLibraries,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
val contributedExamples = sketchbookFolder.resolve("examples")
|
||||
.listFiles{ it.isDirectory }
|
||||
?.map {
|
||||
val properties = it.resolve("examples.properties")
|
||||
val name = findNameInProperties(properties) ?: it.name
|
||||
|
||||
val sketches = getSketches(it.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name,
|
||||
path = it.absolutePath,
|
||||
mode = "java",
|
||||
children = sketches?.children ?: emptyList(),
|
||||
sketches = sketches?.sketches ?: emptyList(),
|
||||
)
|
||||
}
|
||||
?: emptyList()
|
||||
val contributedExamplesFolder = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Contributed Examples",
|
||||
path = sketchbookFolder.resolve("examples").absolutePath,
|
||||
mode = "java",
|
||||
children = contributedExamples,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
val json = serializer.encodeToString(javaModeExamples + javaModeLibraries + contributedLibrariesFolder + contributedExamplesFolder)
|
||||
val json = serializer.encodeToString(listAllExamples())
|
||||
println(json)
|
||||
}
|
||||
|
||||
private fun findNameInProperties(properties: File): String? {
|
||||
if (!properties.exists()) return null
|
||||
companion object {
|
||||
/**
|
||||
* Get all example sketch folders
|
||||
* @return List of example sketch folders
|
||||
*/
|
||||
fun listAllExamples(): List<Sketch.Companion.Folder?> {
|
||||
// TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now
|
||||
// TODO: Allow the user to change the sketchbook location
|
||||
// TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode
|
||||
// TODO: Make non-blocking
|
||||
// TODO: Add tests
|
||||
|
||||
return properties.readLines().firstNotNullOfOrNull { line ->
|
||||
line.split("=", limit = 2)
|
||||
.takeIf { it.size == 2 && it[0].trim() == "name" }
|
||||
?.let { it[1].trim() }
|
||||
val sketchbookFolder = Platform.getDefaultSketchbookFolder()
|
||||
val resourcesDir = System.getProperty("compose.application.resources.dir")
|
||||
|
||||
val javaMode = "$resourcesDir/modes/java"
|
||||
|
||||
val javaModeExamples = File("$javaMode/examples")
|
||||
.listFiles()
|
||||
?.map { getSketches(it) }
|
||||
?: emptyList()
|
||||
|
||||
val javaModeLibrariesExamples = File("$javaMode/libraries")
|
||||
.listFiles { it.isDirectory }
|
||||
?.map { library ->
|
||||
val properties = library.resolve("library.properties")
|
||||
val name = findNameInProperties(properties) ?: library.name
|
||||
|
||||
val libraryExamples = getSketches(library.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = name,
|
||||
path = library.absolutePath,
|
||||
mode = "java",
|
||||
children = libraryExamples?.children ?: emptyList(),
|
||||
sketches = libraryExamples?.sketches ?: emptyList()
|
||||
)
|
||||
} ?: emptyList()
|
||||
val javaModeLibraries = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Libraries",
|
||||
path = "$javaMode/libraries",
|
||||
mode = "java",
|
||||
children = javaModeLibrariesExamples,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
val contributedLibraries = sketchbookFolder.resolve("libraries")
|
||||
.listFiles { it.isDirectory }
|
||||
?.map { library ->
|
||||
val properties = library.resolve("library.properties")
|
||||
val name = findNameInProperties(properties) ?: library.name
|
||||
// Get library name from library.properties if it exists
|
||||
val libraryExamples = getSketches(library.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = name,
|
||||
path = library.absolutePath,
|
||||
mode = "java",
|
||||
children = libraryExamples?.children ?: emptyList(),
|
||||
sketches = libraryExamples?.sketches ?: emptyList()
|
||||
)
|
||||
} ?: emptyList()
|
||||
|
||||
val contributedLibrariesFolder = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Contributed Libraries",
|
||||
path = sketchbookFolder.resolve("libraries").absolutePath,
|
||||
mode = "java",
|
||||
children = contributedLibraries,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
val contributedExamples = sketchbookFolder.resolve("examples")
|
||||
.listFiles { it.isDirectory }
|
||||
?.map {
|
||||
val properties = it.resolve("examples.properties")
|
||||
val name = findNameInProperties(properties) ?: it.name
|
||||
|
||||
val sketches = getSketches(it.resolve("examples"))
|
||||
Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name,
|
||||
path = it.absolutePath,
|
||||
mode = "java",
|
||||
children = sketches?.children ?: emptyList(),
|
||||
sketches = sketches?.sketches ?: emptyList(),
|
||||
)
|
||||
}
|
||||
?: emptyList()
|
||||
val contributedExamplesFolder = Sketch.Companion.Folder(
|
||||
type = "folder",
|
||||
name = "Contributed Examples",
|
||||
path = sketchbookFolder.resolve("examples").absolutePath,
|
||||
mode = "java",
|
||||
children = contributedExamples,
|
||||
sketches = emptyList()
|
||||
)
|
||||
|
||||
return javaModeExamples + javaModeLibraries + contributedLibrariesFolder + contributedExamplesFolder
|
||||
}
|
||||
|
||||
private fun findNameInProperties(properties: File): String? {
|
||||
if (!properties.exists()) return null
|
||||
|
||||
return properties.readLines().firstNotNullOfOrNull { line ->
|
||||
line.split("=", limit = 2)
|
||||
.takeIf { it.size == 2 && it[0].trim() == "name" }
|
||||
?.let { it[1].trim() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
package processing.app.contrib.ui
|
||||
|
||||
import androidx.compose.animation.Animatable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposePanel
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import kotlinx.serialization.Serializable
|
||||
import processing.app.Platform
|
||||
import processing.app.loadPreferences
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.io.path.*
|
||||
|
||||
|
||||
fun main() = application {
|
||||
Window(onCloseRequest = ::exitApplication) {
|
||||
contributionsManager()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
VALID,
|
||||
BROKEN,
|
||||
DEPRECATED
|
||||
}
|
||||
enum class Type {
|
||||
library,
|
||||
mode,
|
||||
tool,
|
||||
examples,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
val name: String,
|
||||
val url: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Contribution(
|
||||
val id: Int,
|
||||
val status: Status,
|
||||
val source: String,
|
||||
val type: Type,
|
||||
val name: String? = null,
|
||||
val categories: List<String>? = emptyList(),
|
||||
val authors: String? = null,
|
||||
val authorList: List<Author>? = emptyList(),
|
||||
val url: String? = null,
|
||||
val sentence: String? = null,
|
||||
val paragraph: String? = null,
|
||||
val version: String? = null,
|
||||
val prettyVersion: String? = null,
|
||||
val minRevision: Int? = null,
|
||||
val maxRevision: Int? = null,
|
||||
val download: String? = null,
|
||||
val isUpdate: Boolean? = null,
|
||||
val isInstalled: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Contributions(
|
||||
val contributions: List<Contribution>
|
||||
)
|
||||
|
||||
fun openContributionsManager(){
|
||||
// open the compose window
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
val frame = JFrame("Contributions Manager")
|
||||
frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
|
||||
frame.setSize(800, 600)
|
||||
|
||||
val composePanel = ComposePanel()
|
||||
composePanel.setContent {
|
||||
contributionsManager()
|
||||
}
|
||||
|
||||
frame.contentPane.add(composePanel)
|
||||
frame.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun contributionsManager(){
|
||||
var contributions by remember { mutableStateOf(listOf<Contribution>()) }
|
||||
var localContributions by remember { mutableStateOf(listOf<Contribution>()) }
|
||||
var error by remember { mutableStateOf<Exception?>(null) }
|
||||
|
||||
val preferences = loadPreferences()
|
||||
|
||||
LaunchedEffect(preferences){
|
||||
try {
|
||||
localContributions = loadContributionProperties(preferences)
|
||||
.map { (type, props) ->
|
||||
Contribution(
|
||||
id = 0,
|
||||
status = Status.VALID,
|
||||
source = "local",
|
||||
type = type,
|
||||
name = props.getProperty("name"),
|
||||
authors = props.getProperty("authors"),
|
||||
url = props.getProperty("url"),
|
||||
sentence = props.getProperty("sentence"),
|
||||
paragraph = props.getProperty("paragraph"),
|
||||
version = props.getProperty("version"),
|
||||
prettyVersion = props.getProperty("prettyVersion"),
|
||||
minRevision = props.getProperty("minRevision")?.toIntOrNull(),
|
||||
maxRevision = props.getProperty("maxRevision")?.toIntOrNull(),
|
||||
download = props.getProperty("download"),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception){
|
||||
error = e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(Unit){
|
||||
try {
|
||||
val url = URL("https://github.com/mingness/processing-contributions-new/raw/refs/heads/main/contributions.yaml")
|
||||
val connection = url.openConnection()
|
||||
val inputStream = connection.getInputStream()
|
||||
val yaml = inputStream.readAllBytes().decodeToString()
|
||||
// TODO cache yaml in processing folder
|
||||
|
||||
val parser = Yaml(
|
||||
configuration = YamlConfiguration(
|
||||
strictMode = false
|
||||
)
|
||||
)
|
||||
val result = parser.decodeFromString(Contributions.serializer(), yaml)
|
||||
|
||||
contributions = result.contributions
|
||||
.filter { it.status == Status.VALID }
|
||||
.map {
|
||||
// TODO Parse better
|
||||
val authorList = it.authors?.split(",")?.map { author ->
|
||||
val parts = author.split("](")
|
||||
val name = parts[0].removePrefix("[")
|
||||
val url = parts.getOrNull(1)?.removeSuffix(")")
|
||||
Author(name, url)
|
||||
} ?: emptyList()
|
||||
it.copy(authorList = authorList)
|
||||
}
|
||||
} catch (e: Exception){
|
||||
error = e
|
||||
}
|
||||
}
|
||||
if(error != null){
|
||||
Text("Error loading contributions: ${error?.message}")
|
||||
return
|
||||
}
|
||||
if(contributions.isEmpty()){
|
||||
Text("Loading contributions...")
|
||||
return
|
||||
}
|
||||
|
||||
val contributionsByType = (contributions + localContributions)
|
||||
.groupBy { it.name }
|
||||
.map { (_, contributions) ->
|
||||
if(contributions.size == 1) return@map contributions.first()
|
||||
else{
|
||||
// check if they all have the same version, otherwise return the newest version
|
||||
val versions = contributions.mapNotNull { it.version }
|
||||
if(versions.toSet().size == 1) return@map contributions.first().copy(isInstalled = true)
|
||||
else{
|
||||
val newest = contributions.maxByOrNull { it.version?.toIntOrNull() ?: 0 }
|
||||
if(newest != null) return@map newest.copy(isUpdate = true, isInstalled = true)
|
||||
else return@map contributions.first().copy(isUpdate = true, isInstalled = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.groupBy { it.type }
|
||||
|
||||
val types = Type.entries
|
||||
var selectedType by remember { mutableStateOf(types.first()) }
|
||||
val contributionsForType = (contributionsByType[selectedType] ?: emptyList())
|
||||
.sortedBy { it.name }
|
||||
|
||||
var selectedContribution by remember { mutableStateOf<Contribution?>(null) }
|
||||
Box{
|
||||
Column {
|
||||
Row{
|
||||
for(type in types){
|
||||
val background = remember { Animatable(Color.Transparent) }
|
||||
val color = remember { Animatable(Color.Black) }
|
||||
LaunchedEffect(selectedType){
|
||||
if(selectedType == type){
|
||||
background.animateTo(Color(0xff0251c8))
|
||||
color.animateTo(Color.White)
|
||||
}else{
|
||||
background.animateTo(Color.Transparent)
|
||||
color.animateTo(Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier
|
||||
.background(background.value)
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.clickable {
|
||||
selectedType = type
|
||||
selectedContribution = null
|
||||
}
|
||||
.padding(16.dp, 8.dp)
|
||||
){
|
||||
Text(type.name, color = color.value)
|
||||
val updates = contributionsByType[type]?.count { it.isUpdate == true } ?: 0
|
||||
if(updates > 0){
|
||||
Text("($updates)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f)){
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(state = state) {
|
||||
item{
|
||||
// Table Header
|
||||
}
|
||||
items(contributionsForType){ contribution ->
|
||||
Row(modifier = Modifier
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.clickable { selectedContribution = contribution }
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.weight(1f)){
|
||||
if(contribution.isUpdate == true){
|
||||
Text("Update")
|
||||
}else if(contribution.isInstalled == true){
|
||||
Text("Installed")
|
||||
}
|
||||
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(8f)){
|
||||
Text(contribution.name ?: "Unnamed", fontWeight = FontWeight.Bold)
|
||||
Text(contribution.sentence ?: "No description", maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row(modifier = Modifier.weight(4f)){
|
||||
Text(contribution.authorList?.joinToString { it.name } ?: "Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.background(Color.LightGray)
|
||||
.fillMaxHeight(),
|
||||
adapter = rememberScrollbarAdapter(
|
||||
scrollState = state
|
||||
)
|
||||
)
|
||||
}
|
||||
ContributionPane(
|
||||
contribution = selectedContribution,
|
||||
onClose = { selectedContribution = null }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun loadContributionProperties(preferences: Properties): List<Pair<Type, Properties>>{
|
||||
val result = mutableListOf<Pair<Type, Properties>>()
|
||||
val sketchBook = Path(preferences.getProperty("sketchbook.path.four", Platform.getDefaultSketchbookFolder().path))
|
||||
sketchBook.forEachDirectoryEntry{ contributionsFolder ->
|
||||
if(!contributionsFolder.isDirectory()) return@forEachDirectoryEntry
|
||||
val typeName = contributionsFolder.fileName.toString()
|
||||
val type: Type = when(typeName){
|
||||
"libraries" -> Type.library
|
||||
"modes" -> Type.mode
|
||||
"tools" -> Type.tool
|
||||
"examples" -> Type.examples
|
||||
else -> return@forEachDirectoryEntry
|
||||
}
|
||||
contributionsFolder.forEachDirectoryEntry { contribution ->
|
||||
if(!contribution.isDirectory()) return@forEachDirectoryEntry
|
||||
contribution.forEachDirectoryEntry("*.properties"){ entry ->
|
||||
val props = Properties()
|
||||
props.load(entry.inputStream())
|
||||
result += Pair(type, props)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package processing.app.contrib.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Window
|
||||
|
||||
//--processing-blue-light: #82afff;
|
||||
//--processing-blue-mid: #0564ff;
|
||||
//--processing-blue-deep: #1e32aa;
|
||||
//--processing-blue-dark: #0f195a;
|
||||
//--processing-blue: #0251c8;
|
||||
|
||||
@Composable
|
||||
fun ContributionPane(contribution: Contribution?, onClose: () -> Unit) {
|
||||
if(contribution == null) {
|
||||
return
|
||||
}
|
||||
val typeName = when(contribution.type) {
|
||||
Type.library -> "Library"
|
||||
Type.tool -> "Tool"
|
||||
Type.examples -> "Example"
|
||||
Type.mode -> "Mode"
|
||||
}
|
||||
Window(
|
||||
title = "${typeName}: ${contribution.name}",
|
||||
onCloseRequest = onClose,
|
||||
onKeyEvent = {
|
||||
if(it.key == Key.Escape) {
|
||||
onClose()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
){
|
||||
Box {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
Text(typeName, style = TextStyle(fontSize = 16.sp))
|
||||
Text(contribution.name ?: "", style = TextStyle(fontSize = 20.sp))
|
||||
Row(modifier = Modifier.padding(0.dp, 10.dp)) {
|
||||
val action = when(contribution.isUpdate) {
|
||||
true -> "Update"
|
||||
false, null -> when(contribution.isInstalled) {
|
||||
true -> "Uninstall"
|
||||
false, null -> "Install"
|
||||
}
|
||||
}
|
||||
Text(action,
|
||||
style = TextStyle(fontSize = 14.sp, color = Color.White),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
|
||||
}
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.background(Color(0xff0251c8))
|
||||
.padding(24.dp,12.dp)
|
||||
)
|
||||
}
|
||||
Text(contribution.paragraph ?: "", style = TextStyle(fontSize = 14.sp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,8 +37,6 @@ public class LinuxPlatform extends DefaultPlatform {
|
||||
|
||||
public void initBase(Base base) {
|
||||
super.initBase(base);
|
||||
|
||||
JFrame.setDefaultLookAndFeelDecorated(true);
|
||||
System.setProperty("flatlaf.menuBarEmbedded", "true");
|
||||
|
||||
// Set X11 WM_CLASS property which is used as the application
|
||||
|
||||
@@ -23,38 +23,42 @@
|
||||
|
||||
package processing.app.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.print.*;
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.stream.Collectors;
|
||||
import com.formdev.flatlaf.util.SystemInfo;
|
||||
import processing.app.*;
|
||||
import processing.app.Formatter;
|
||||
import processing.app.contrib.ContributionManager;
|
||||
import processing.app.laf.PdeMenuItemUI;
|
||||
import processing.app.syntax.*;
|
||||
import processing.core.PApplet;
|
||||
import processing.utils.SketchException;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.*;
|
||||
import javax.swing.plaf.basic.*;
|
||||
import javax.swing.text.*;
|
||||
import javax.swing.text.html.*;
|
||||
import javax.swing.undo.*;
|
||||
|
||||
import com.formdev.flatlaf.util.SystemInfo;
|
||||
import processing.app.*;
|
||||
import processing.utils.SketchException;
|
||||
import processing.app.contrib.ContributionManager;
|
||||
import processing.app.laf.PdeMenuItemUI;
|
||||
import processing.app.syntax.*;
|
||||
import processing.core.*;
|
||||
import javax.swing.plaf.basic.BasicSplitPaneDivider;
|
||||
import javax.swing.plaf.basic.BasicSplitPaneUI;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Element;
|
||||
import javax.swing.text.View;
|
||||
import javax.swing.text.ViewFactory;
|
||||
import javax.swing.text.html.HTMLEditorKit;
|
||||
import javax.swing.undo.CannotRedoException;
|
||||
import javax.swing.undo.CannotUndoException;
|
||||
import javax.swing.undo.CompoundEdit;
|
||||
import javax.swing.undo.UndoManager;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
@@ -207,6 +211,10 @@ public abstract class Editor extends JFrame implements RunnerListener {
|
||||
spacer.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
box.add(spacer);
|
||||
}
|
||||
if (Platform.isLinux()) {
|
||||
setUndecorated(true);
|
||||
getRootPane().setWindowDecorationStyle(JRootPane.FRAME);
|
||||
}
|
||||
|
||||
rebuildModePopup();
|
||||
toolbar = createToolbar();
|
||||
@@ -1057,6 +1065,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
|
||||
var updateTrigger = new JMenuItem(Language.text("menu.develop.check_for_updates"));
|
||||
updateTrigger.addActionListener(e -> {
|
||||
Preferences.unset("update.last");
|
||||
Preferences.setInteger("update.beta_welcome", 0);
|
||||
new UpdateCheck(base);
|
||||
});
|
||||
developMenu.add(updateTrigger);
|
||||
|
||||
630
app/src/processing/app/ui/PDEWelcome.kt
Normal file
630
app/src/processing/app/ui/PDEWelcome.kt
Normal file
@@ -0,0 +1,630 @@
|
||||
package processing.app.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.EaseInOut
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.NoteAdd
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.decodeToImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.onPointerEvent
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.application
|
||||
import processing.app.*
|
||||
import processing.app.api.Contributions.ExamplesList.Companion.listAllExamples
|
||||
import processing.app.api.Sketch.Companion.Sketch
|
||||
import processing.app.ui.theme.*
|
||||
import java.io.File
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.exists
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PDEWelcome(base: Base? = null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest),
|
||||
){
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
val xsPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||
val xsModifier = Modifier
|
||||
.defaultMinSize(minHeight = 1.dp)
|
||||
.height(32.dp)
|
||||
val textColor = if(isSystemInDarkTheme()) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSecondaryContainer
|
||||
val locale = LocalLocale.current
|
||||
|
||||
/**
|
||||
* Left main column
|
||||
*/
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(0.8f)
|
||||
.padding(
|
||||
top = 48.dp,
|
||||
start = 56.dp,
|
||||
end = 64.dp,
|
||||
bottom = 56.dp
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Title row
|
||||
*/
|
||||
Row (
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
){
|
||||
Image(
|
||||
painter = painterResource("logo.svg"),
|
||||
modifier = Modifier
|
||||
.size(50.dp),
|
||||
contentDescription = locale["welcome.processing.logo"]
|
||||
)
|
||||
Text(
|
||||
text = locale["welcome.processing.title"],
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
color = textColor,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterVertically),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
){
|
||||
val showLanguageMenu = remember { mutableStateOf(false) }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
showLanguageMenu.value = !showLanguageMenu.value
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
shape = shape
|
||||
){
|
||||
Icon(Icons.Default.Language, contentDescription = "", modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(text = locale.locale.displayName)
|
||||
Icon(Icons.Default.ArrowDropDown, contentDescription = "", modifier = Modifier.size(20.dp))
|
||||
languagesDropdown(showLanguageMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* New sketch, examples, sketchbook card
|
||||
*/
|
||||
val colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = textColor
|
||||
)
|
||||
Column{
|
||||
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
|
||||
val medModifier = Modifier
|
||||
.sizeIn(minHeight = 56.dp)
|
||||
TextButton(
|
||||
onClick = {
|
||||
base?.handleNew() ?: noBaseWarning()
|
||||
},
|
||||
colors = colors,
|
||||
modifier = medModifier,
|
||||
shape = shape
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Outlined.NoteAdd, contentDescription = "")
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(locale["welcome.actions.sketch.new"])
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
base?.let{
|
||||
base.showSketchbookFrame()
|
||||
} ?: noBaseWarning()
|
||||
},
|
||||
colors = colors,
|
||||
modifier = medModifier,
|
||||
shape = shape
|
||||
) {
|
||||
Icon(Icons.Outlined.FolderOpen, contentDescription = "")
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(locale["welcome.actions.sketchbook"], modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
base?.let{
|
||||
base.showExamplesFrame()
|
||||
} ?: noBaseWarning()
|
||||
},
|
||||
colors = colors,
|
||||
modifier = medModifier,
|
||||
shape = shape
|
||||
) {
|
||||
Icon(Icons.Outlined.FolderSpecial, contentDescription = "")
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(locale["welcome.actions.examples"])
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resources and community card
|
||||
*/
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
){
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(48.dp),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = 18.dp,
|
||||
end = 24.dp,
|
||||
bottom = 24.dp,
|
||||
start = 24.dp
|
||||
)
|
||||
) {
|
||||
val colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = locale["welcome.resources.title"],
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://processing.org/tutorials/gettingstarted")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(Icons.Outlined.PinDrop, contentDescription = "", modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = locale["welcome.resources.get_started"],
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://processing.org/tutorials")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(Icons.Outlined.School, contentDescription = "", modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = locale["welcome.resources.tutorials"],
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://processing.org/reference")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(Icons.Outlined.Book, contentDescription = "", modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = locale["welcome.resources.documentation"],
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = locale["welcome.community.title"],
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(48.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://discourse.processing.org")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.ChatBubbleOutline,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = locale["welcome.community.forum"]
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://discord.processing.org")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(
|
||||
painterResource("icons/Discord.svg"),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Discord")
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://github.com/processing/processing4")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(
|
||||
painterResource("icons/GitHub.svg"),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("GitHub")
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Platform.openURL("https://www.instagram.com/processing_core/")
|
||||
},
|
||||
contentPadding = xsPadding,
|
||||
modifier = xsModifier,
|
||||
colors = colors
|
||||
) {
|
||||
Icon(
|
||||
painterResource("icons/Instagram.svg"),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Instagram")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Show on startup checkbox
|
||||
*/
|
||||
Row{
|
||||
val preferences = LocalPreferences.current
|
||||
val showOnStartup = preferences["welcome.four.show"].toBoolean()
|
||||
fun toggle(next: Boolean? = null) {
|
||||
preferences["welcome.four.show"] = (next ?: !showOnStartup).toString()
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable(onClick = ::toggle)
|
||||
.padding(end = 8.dp)
|
||||
.height(32.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = showOnStartup,
|
||||
onCheckedChange = ::toggle,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.tertiary
|
||||
),
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 1.dp)
|
||||
)
|
||||
Text(
|
||||
text = locale["welcome.actions.show_startup"],
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Examples list
|
||||
*/
|
||||
val scrollMargin = 35.dp
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(350.dp + scrollMargin)
|
||||
) {
|
||||
val examples = remember { mutableStateListOf(
|
||||
*listOf(
|
||||
Platform.getContentFile("modes/java/examples/Basics/Arrays/Array"),
|
||||
Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective"),
|
||||
Platform.getContentFile("modes/java/examples/Basics/Color/Brightness"),
|
||||
Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")
|
||||
).map{ Sketch(path = it.absolutePath, name = it.name) }.toTypedArray()
|
||||
)}
|
||||
|
||||
remember {
|
||||
val sketches = mutableListOf<Sketch>()
|
||||
val sketchFolders = listAllExamples()
|
||||
fun gatherSketches(folder: processing.app.api.Sketch.Companion.Folder?) {
|
||||
if (folder == null) return
|
||||
sketches.addAll(folder.sketches.filter { it -> Path(it.path).resolve("${it.name}.png").exists() })
|
||||
folder.children.forEach { child ->
|
||||
gatherSketches(child)
|
||||
}
|
||||
}
|
||||
sketchFolders.forEach { folder ->
|
||||
gatherSketches(folder)
|
||||
}
|
||||
if(sketches.isEmpty()) {
|
||||
return@remember
|
||||
}
|
||||
examples.clear()
|
||||
examples.addAll(sketches.shuffled().take(20))
|
||||
}
|
||||
val state = rememberLazyListState(
|
||||
initialFirstVisibleItemScrollOffset = 150
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = state,
|
||||
contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp, end = 20.dp, start = scrollMargin),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(examples) { example ->
|
||||
example.card{
|
||||
base?.let {
|
||||
base.handleOpen("${example.path}/${example.name}.pde")
|
||||
} ?: noBaseWarning()
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterEnd),
|
||||
adapter = rememberScrollbarAdapter(state)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun Sketch.card(onOpen: () -> Unit = {}) {
|
||||
val locale = LocalLocale.current
|
||||
val sketch = this
|
||||
var hovered by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
Modifier
|
||||
.border(
|
||||
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.fillMaxSize()
|
||||
.aspectRatio(16 / 9f)
|
||||
.onPointerEvent(PointerEventType.Enter) {
|
||||
hovered = true
|
||||
}
|
||||
.onPointerEvent(PointerEventType.Exit) {
|
||||
hovered = false
|
||||
}
|
||||
) {
|
||||
val image = remember {
|
||||
File(sketch.path, "${sketch.name}.png").takeIf { it.exists() }
|
||||
}
|
||||
if (image == null) {
|
||||
Icon(
|
||||
painter = painterResource("logo.svg"),
|
||||
modifier = Modifier
|
||||
.size(75.dp)
|
||||
.align(Alignment.Center),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
contentDescription = "Processing Logo"
|
||||
)
|
||||
HorizontalDivider()
|
||||
} else {
|
||||
val imageBitmap: ImageBitmap = remember(image) {
|
||||
image.inputStream().readAllBytes().decodeToImageBitmap()
|
||||
}
|
||||
Image(
|
||||
painter = BitmapPainter(imageBitmap),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentDescription = sketch.name
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
) {
|
||||
val duration = 150
|
||||
AnimatedVisibility(
|
||||
visible = hovered,
|
||||
enter = slideIn(
|
||||
initialOffset = { fullSize -> IntOffset(0, fullSize.height) },
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = EaseInOut
|
||||
)
|
||||
),
|
||||
exit = slideOut(
|
||||
targetOffset = { fullSize -> IntOffset(0, fullSize.height) },
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(12.dp)
|
||||
.padding(start = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = sketch.name,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
)
|
||||
Button(
|
||||
onClick = onOpen,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiary
|
||||
),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 12.dp,
|
||||
vertical = 4.dp
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = locale["welcome.sketch.open"],
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun noBaseWarning() {
|
||||
Messages.showWarning(
|
||||
"No Base",
|
||||
"No Base instance provided, this ui is likely being previewed."
|
||||
)
|
||||
}
|
||||
|
||||
val size = DpSize(970.dp, 600.dp)
|
||||
const val titleKey = "menu.help.welcome"
|
||||
class WelcomeScreen
|
||||
|
||||
fun showWelcomeScreen(base: Base? = null) {
|
||||
PDESwingWindow(
|
||||
titleKey = titleKey,
|
||||
size = size.toDimension(),
|
||||
unique = WelcomeScreen::class,
|
||||
fullWindowContent = true
|
||||
) {
|
||||
PDEWelcomeWithSurvey(base)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun languagesDropdown(showOptions: MutableState<Boolean>) {
|
||||
val locale = LocalLocale.current
|
||||
val languages = if (Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English")
|
||||
DropdownMenu(
|
||||
expanded = showOptions.value,
|
||||
onDismissRequest = {
|
||||
showOptions.value = false
|
||||
},
|
||||
) {
|
||||
languages.forEach { family ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(family.value) },
|
||||
onClick = {
|
||||
locale.set(java.util.Locale(family.key))
|
||||
showOptions.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PDEWelcomeWithSurvey(base: Base? = null) {
|
||||
Box {
|
||||
PDEWelcome(base)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.shadow(
|
||||
elevation = 5.dp,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
) {
|
||||
SurveyInvitation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main(){
|
||||
application {
|
||||
PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) {
|
||||
PDETheme(darkTheme = true) {
|
||||
PDEWelcomeWithSurvey()
|
||||
}
|
||||
}
|
||||
PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) {
|
||||
PDETheme(darkTheme = false) {
|
||||
PDEWelcomeWithSurvey()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ class Start {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
val composition = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
Toolkit.setIcon(window)
|
||||
|
||||
visible = true
|
||||
composition.launch {
|
||||
delay(duration.toLong() + timeMargin)
|
||||
|
||||
@@ -5,8 +5,9 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposePanel
|
||||
@@ -18,54 +19,54 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import processing.app.Platform
|
||||
import processing.app.ui.theme.LocalLocale
|
||||
import processing.app.ui.theme.ProcessingTheme
|
||||
import processing.app.ui.theme.PDETheme
|
||||
import javax.swing.JComponent
|
||||
|
||||
|
||||
fun addSurveyToWelcomeScreen(): JComponent {
|
||||
return ComposePanel().apply {
|
||||
setContent {
|
||||
ProcessingTheme {
|
||||
val locale = LocalLocale.current
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(420.dp)
|
||||
.padding(16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.clickable {
|
||||
Platform.openURL("https://survey.processing.org/")
|
||||
}
|
||||
.pointerHoverIcon(
|
||||
PointerIcon.Hand
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("bird.svg"),
|
||||
contentDescription = locale["beta.logo"],
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(20.dp)
|
||||
.size(50.dp)
|
||||
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = locale["welcome.survey.title"],
|
||||
style = MaterialTheme.typography.subtitle1.copy(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
Text(
|
||||
text = locale["welcome.survey.description"],
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
PDETheme {
|
||||
SurveyInvitation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SurveyInvitation() {
|
||||
val locale = LocalLocale.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(420.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
.clickable {
|
||||
Platform.openURL("https://survey.processing.org/")
|
||||
}
|
||||
.pointerHoverIcon(
|
||||
PointerIcon.Hand
|
||||
)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("bird.svg"),
|
||||
contentDescription = locale["beta.logo"],
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(20.dp)
|
||||
.size(50.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = locale["welcome.survey.title"],
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
Text(
|
||||
text = locale["welcome.survey.description"],
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,65 @@
|
||||
package processing.app.ui
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.MaterialTheme.typography
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposePanel
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.painterResource
|
||||
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 com.mikepenz.markdown.compose.Markdown
|
||||
import com.mikepenz.markdown.m2.markdownColor
|
||||
import com.mikepenz.markdown.m2.markdownTypography
|
||||
import com.mikepenz.markdown.m3.markdownColor
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import processing.app.Base.getRevision
|
||||
import processing.app.Base.getVersionName
|
||||
import processing.app.Preferences
|
||||
import processing.app.ui.theme.LocalLocale
|
||||
import processing.app.ui.theme.LocalTheme
|
||||
import processing.app.ui.theme.Locale
|
||||
import processing.app.ui.theme.ProcessingTheme
|
||||
import java.awt.Cursor
|
||||
import processing.app.ui.theme.PDEComposeWindow
|
||||
import processing.app.ui.theme.PDESwingWindow
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
|
||||
class WelcomeToBeta {
|
||||
companion object{
|
||||
val windowSize = Dimension(400, 200)
|
||||
val windowTitle = Locale()["beta.window.title"]
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun showWelcomeToBeta() {
|
||||
val mac = SystemInfo.isMacFullWindowContentSupported
|
||||
SwingUtilities.invokeLater {
|
||||
JFrame(windowTitle).apply {
|
||||
val close = {
|
||||
Preferences.set("update.beta_welcome", getRevision().toString())
|
||||
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()
|
||||
val close = {
|
||||
Preferences.set("update.beta_welcome", getRevision().toString())
|
||||
}
|
||||
|
||||
PDESwingWindow("beta.window.title", onClose = close, size = windowSize) {
|
||||
welcomeToBeta(close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val windowSize = Dimension(500, 300)
|
||||
|
||||
@Composable
|
||||
fun welcomeToBeta(close: () -> Unit = {}) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(20.dp, 10.dp)
|
||||
.size(windowSize.width.dp, windowSize.height.dp),
|
||||
.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement
|
||||
.spacedBy(20.dp)
|
||||
){
|
||||
) {
|
||||
val locale = LocalLocale.current
|
||||
Image(
|
||||
painter = painterResource("bird.svg"),
|
||||
contentDescription = locale["beta.logo"],
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(100.dp, 100.dp)
|
||||
.size(120.dp)
|
||||
.offset(0.dp, (-25).dp)
|
||||
)
|
||||
Column(
|
||||
@@ -117,7 +73,7 @@ class WelcomeToBeta {
|
||||
) {
|
||||
Text(
|
||||
text = locale["beta.title"],
|
||||
style = typography.subtitle1,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
val text = locale["beta.message"]
|
||||
.replace('$' + "version", getVersionName())
|
||||
@@ -125,85 +81,34 @@ class WelcomeToBeta {
|
||||
Markdown(
|
||||
text,
|
||||
colors = markdownColor(),
|
||||
typography = markdownTypography(text = typography.body1, link = typography.body1.copy(color = colors.primary)),
|
||||
typography = markdownTypography(),
|
||||
modifier = Modifier.background(Color.Transparent).padding(bottom = 10.dp)
|
||||
)
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
PDEButton(onClick = {
|
||||
Button(onClick = {
|
||||
close()
|
||||
}) {
|
||||
Text(
|
||||
text = locale["beta.button"],
|
||||
color = colors.onPrimary
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PDEButton(onClick: () -> Unit, modifier: Modifier = Modifier, 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()
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides colors.onPrimary
|
||||
) {
|
||||
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<String>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
PDEComposeWindow(
|
||||
titleKey = "beta.window.title",
|
||||
onClose = ::exitApplication,
|
||||
size = DpSize(windowSize.width.dp, windowSize.height.dp)
|
||||
) {
|
||||
welcomeToBeta {
|
||||
exitApplication()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
app/src/processing/app/ui/theme/Colors.kt
Normal file
90
app/src/processing/app/ui/theme/Colors.kt
Normal file
@@ -0,0 +1,90 @@
|
||||
package processing.app.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
class ProcessingColors{
|
||||
companion object{
|
||||
val blue = Color(0xFF0251c8)
|
||||
val lightBlue = Color(0xFF82AFFF)
|
||||
|
||||
val deepBlue = Color(0xFF1e32aa)
|
||||
val darkBlue = Color(0xFF0F195A)
|
||||
|
||||
val white = Color(0xFFFFFFFF)
|
||||
val lightGray = Color(0xFFF5F5F5)
|
||||
val gray = Color(0xFFDBDBDB)
|
||||
val darkGray = Color(0xFF898989)
|
||||
val darkerGray = Color(0xFF727070)
|
||||
val veryDarkGray = Color(0xFF1E1E1E)
|
||||
val black = Color(0xFF0D0D0D)
|
||||
|
||||
val error = Color(0xFFFF5757)
|
||||
val errorContainer = Color(0xFFFFA6A6)
|
||||
|
||||
val p5Light = Color(0xFFfd9db9)
|
||||
val p5Mid = Color(0xFFff4077)
|
||||
val p5Dark = Color(0xFFaf1f42)
|
||||
|
||||
val foundationLight = Color(0xFFd4b2fe)
|
||||
val foundationMid = Color(0xFF9c4bff)
|
||||
val foundationDark = Color(0xFF5501a4)
|
||||
|
||||
val downloadInactive = Color(0xFF8890B3)
|
||||
val downloadBackgroundActive = Color(0xFF14508B)
|
||||
}
|
||||
}
|
||||
|
||||
val PDELightColor = lightColorScheme(
|
||||
primary = ProcessingColors.blue,
|
||||
onPrimary = ProcessingColors.white,
|
||||
|
||||
primaryContainer = ProcessingColors.downloadBackgroundActive,
|
||||
onPrimaryContainer = ProcessingColors.darkBlue,
|
||||
|
||||
secondary = ProcessingColors.deepBlue,
|
||||
onSecondary = ProcessingColors.white,
|
||||
|
||||
secondaryContainer = ProcessingColors.downloadInactive,
|
||||
onSecondaryContainer = ProcessingColors.white,
|
||||
|
||||
tertiary = ProcessingColors.p5Mid,
|
||||
onTertiary = ProcessingColors.white,
|
||||
|
||||
tertiaryContainer = ProcessingColors.p5Light,
|
||||
onTertiaryContainer = ProcessingColors.p5Dark,
|
||||
|
||||
background = ProcessingColors.white,
|
||||
onBackground = ProcessingColors.darkBlue,
|
||||
|
||||
surface = ProcessingColors.lightGray,
|
||||
onSurface = ProcessingColors.darkerGray,
|
||||
|
||||
error = ProcessingColors.error,
|
||||
onError = ProcessingColors.white,
|
||||
|
||||
errorContainer = ProcessingColors.errorContainer,
|
||||
onErrorContainer = ProcessingColors.white
|
||||
)
|
||||
|
||||
val PDEDarkColor = darkColorScheme(
|
||||
primary = ProcessingColors.deepBlue,
|
||||
onPrimary = ProcessingColors.white,
|
||||
|
||||
secondary = ProcessingColors.lightBlue,
|
||||
onSecondary = ProcessingColors.white,
|
||||
|
||||
tertiary = ProcessingColors.blue,
|
||||
onTertiary = ProcessingColors.white,
|
||||
|
||||
background = ProcessingColors.veryDarkGray,
|
||||
onBackground = ProcessingColors.white,
|
||||
|
||||
surface = ProcessingColors.darkerGray,
|
||||
onSurface = ProcessingColors.lightGray,
|
||||
|
||||
error = ProcessingColors.error,
|
||||
onError = ProcessingColors.white,
|
||||
)
|
||||
@@ -1,17 +1,31 @@
|
||||
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.Messages
|
||||
import processing.app.PlatformStart
|
||||
import processing.app.watchFile
|
||||
import processing.utils.Settings
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
class Locale(language: String = "") : Properties() {
|
||||
/**
|
||||
* The Locale class extends the standard Java Properties class
|
||||
* to provide localization capabilities.
|
||||
* It loads localization resources from property files based on the specified language code.
|
||||
* The class also provides a method to change the current locale and update the application accordingly.
|
||||
* Usage:
|
||||
* ```
|
||||
* val locale = Locale("es") { newLocale ->
|
||||
* // Handle locale change, e.g., update UI or restart application
|
||||
* }
|
||||
* val localizedString = locale["someKey"]
|
||||
* ```
|
||||
*/
|
||||
class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() {
|
||||
var locale: java.util.Locale = java.util.Locale.getDefault()
|
||||
|
||||
init {
|
||||
val locale = java.util.Locale.getDefault()
|
||||
load(ClassLoader.getSystemResourceAsStream("languages/PDE.properties"))
|
||||
@@ -32,22 +46,88 @@ class Locale(language: String = "") : Properties() {
|
||||
@Deprecated("Use get instead", ReplaceWith("get(key)"))
|
||||
override fun getProperty(key: String?, default: String): String {
|
||||
val value = super.getProperty(key, default)
|
||||
if(value == default) Messages.log("Missing translation for $key")
|
||||
if (value == default) Messages.log("Missing translation for $key")
|
||||
return value
|
||||
}
|
||||
|
||||
operator fun get(key: String): String = getProperty(key, key)
|
||||
fun set(locale: java.util.Locale) {
|
||||
setLocale?.invoke(locale)
|
||||
}
|
||||
}
|
||||
val LocalLocale = compositionLocalOf { Locale() }
|
||||
|
||||
/**
|
||||
* A CompositionLocal to provide access to the Locale instance
|
||||
* throughout the composable hierarchy. see [LocaleProvider]
|
||||
* Usage:
|
||||
* ```
|
||||
* val locale = LocalLocale.current
|
||||
* val localizedString = locale["someKey"]
|
||||
* ```
|
||||
*/
|
||||
val LocalLocale = compositionLocalOf<Locale> { error("No Locale Set") }
|
||||
|
||||
/**
|
||||
* This composable function sets up a locale provider that manages application localization.
|
||||
* It initializes the locale from a language file, watches for changes to that file, and updates
|
||||
* the locale accordingly. It uses a [Locale] class to handle loading of localized resources.
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* LocaleProvider {
|
||||
* // Your app content here
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* To access the locale:
|
||||
* ```
|
||||
* val locale = LocalLocale.current
|
||||
* val localizedString = locale["someKey"]
|
||||
* ```
|
||||
*
|
||||
* To change the locale:
|
||||
* ```
|
||||
* locale.set(java.util.Locale("es"))
|
||||
* ```
|
||||
* This will update the `language.txt` file and reload the locale.
|
||||
*/
|
||||
@Composable
|
||||
fun LocaleProvider(content: @Composable () -> Unit) {
|
||||
PlatformStart()
|
||||
|
||||
val settingsFolder = Settings.getFolder()
|
||||
val languageFile = File(settingsFolder, "language.txt")
|
||||
watchFile(languageFile)
|
||||
|
||||
val locale = Locale(languageFile.readText().substring(0, 2))
|
||||
CompositionLocalProvider(LocalLocale provides locale) {
|
||||
content()
|
||||
remember(languageFile) {
|
||||
if (languageFile.exists()) return@remember
|
||||
Messages.log("Creating language file at ${languageFile.absolutePath}")
|
||||
settingsFolder.mkdirs()
|
||||
languageFile.writeText(java.util.Locale.getDefault().language)
|
||||
}
|
||||
|
||||
val update = watchFile(languageFile)
|
||||
var code by remember(languageFile, update) { mutableStateOf(languageFile.readText().substring(0, 2)) }
|
||||
remember(code) {
|
||||
val locale = java.util.Locale(code)
|
||||
java.util.Locale.setDefault(locale)
|
||||
}
|
||||
|
||||
fun setLocale(locale: java.util.Locale) {
|
||||
Messages.log("Setting locale to ${locale.language}")
|
||||
languageFile.writeText(locale.language)
|
||||
code = locale.language
|
||||
}
|
||||
|
||||
|
||||
val locale = Locale(code, ::setLocale)
|
||||
remember(code) { Messages.log("Loaded Locale: $code") }
|
||||
val dir = when (locale["locale.direction"]) {
|
||||
"rtl" -> LayoutDirection.Rtl
|
||||
else -> LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides dir) {
|
||||
CompositionLocalProvider(LocalLocale provides locale) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,341 @@
|
||||
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
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Map
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import processing.app.LocalPreferences
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
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 darkScheme
|
||||
import lightScheme
|
||||
import processing.app.PreferencesProvider
|
||||
import java.io.InputStream
|
||||
import java.util.Properties
|
||||
|
||||
|
||||
class Theme(themeFile: String? = "") : Properties() {
|
||||
init {
|
||||
load(ClassLoader.getSystemResourceAsStream("theme.txt"))
|
||||
load(ClassLoader.getSystemResourceAsStream(themeFile) ?: InputStream.nullInputStream())
|
||||
}
|
||||
fun getColor(key: String): Color {
|
||||
return Color(getProperty(key).toColorInt())
|
||||
}
|
||||
}
|
||||
|
||||
val LocalTheme = compositionLocalOf<Theme> { error("No theme provided") }
|
||||
|
||||
/**
|
||||
* Processing Theme for Jetpack Compose Desktop
|
||||
* Based on Material3
|
||||
*
|
||||
* Makes Material3 components follow Processing color scheme and typography
|
||||
* We experimented with using the material3 theme builder, but it made it look too Android-y
|
||||
* So we defined our own color scheme and typography based on Processing design guidelines
|
||||
*
|
||||
* This composable also provides Preferences and Locale context to all child composables
|
||||
*
|
||||
* Also, important: sets a default density of 1.25 for better scaling on desktop screens, [LocalDensity]
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* PDETheme {
|
||||
* val pref = LocalPreferences.current
|
||||
* val locale = LocalLocale.current
|
||||
* ...
|
||||
* // Your composables here
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param darkTheme Whether to use dark theme or light theme. Defaults to system setting.
|
||||
*/
|
||||
@Composable
|
||||
fun ProcessingTheme(
|
||||
fun PDETheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable() () -> Unit
|
||||
) {
|
||||
content: @Composable () -> Unit
|
||||
){
|
||||
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")
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
LocaleProvider {
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
LocaleProvider {
|
||||
MaterialTheme(
|
||||
colorScheme = if(darkTheme) darkScheme else lightScheme,
|
||||
typography = PDETypography
|
||||
){
|
||||
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLowest)) {
|
||||
CompositionLocalProvider(
|
||||
LocalScrollbarStyle provides ScrollbarStyle(
|
||||
minimalHeight = 16.dp,
|
||||
thickness = 8.dp,
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
hoverDurationMillis = 300,
|
||||
unhoverColor = MaterialTheme.colorScheme.outlineVariant,
|
||||
hoverColor = MaterialTheme.colorScheme.outlineVariant
|
||||
),
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
|
||||
// LocalDensity provides Density(1.25f, 1.25f),
|
||||
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")
|
||||
/**
|
||||
* Simple app to preview the Processing Theme components
|
||||
* Includes buttons, text fields, checkboxes, sliders, etc.
|
||||
* Run by executing the main() function by clicking the green arrow next to it in intelliJ IDEA
|
||||
*/
|
||||
fun main() {
|
||||
application {
|
||||
val windowState = rememberWindowState(
|
||||
size = DpSize(800.dp, 600.dp),
|
||||
position = WindowPosition(Alignment.Center)
|
||||
)
|
||||
var darkTheme by remember { mutableStateOf(false) }
|
||||
Window(onCloseRequest = ::exitApplication, state = windowState, title = "Processing Theme") {
|
||||
PDETheme(darkTheme = darkTheme) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Processing Theme Components", style = MaterialTheme.typography.titleLarge)
|
||||
Card {
|
||||
Row {
|
||||
Checkbox(darkTheme, onCheckedChange = { darkTheme = !darkTheme })
|
||||
Text(
|
||||
"Dark Theme",
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
val scrollable = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollable),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
ComponentPreview("Colors") {
|
||||
val colors = listOf<Triple<String, Color, Color>>(
|
||||
Triple("Primary", MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary),
|
||||
Triple("Secondary", MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.onSecondary),
|
||||
Triple("Tertiary", MaterialTheme.colorScheme.tertiary, MaterialTheme.colorScheme.onTertiary),
|
||||
Triple("Primary Container", MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.onPrimaryContainer),
|
||||
Triple("Secondary Container", MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.colorScheme.onSecondaryContainer),
|
||||
Triple("Tertiary Container", MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.onTertiaryContainer),
|
||||
Triple("Error Container", MaterialTheme.colorScheme.errorContainer, MaterialTheme.colorScheme.onErrorContainer),
|
||||
Triple("Background", MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.onBackground),
|
||||
Triple("Surface", MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.onSurface),
|
||||
Triple("Surface Variant", MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
Triple("Error", MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.onError),
|
||||
|
||||
Triple("Surface Lowest", MaterialTheme.colorScheme.surfaceContainerLowest, MaterialTheme.colorScheme.onSurface),
|
||||
Triple("Surface Low", MaterialTheme.colorScheme.surfaceContainerLow, MaterialTheme.colorScheme.onSurface),
|
||||
Triple("Surface", MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.colorScheme.onSurface),
|
||||
Triple("Surface High", MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
|
||||
Triple("Surface Highest", MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
Column {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val section = colors.subList(0,3)
|
||||
for((name, color, onColor) in section){
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = color),
|
||||
onClick = {}) {
|
||||
Text(name, color = onColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val section = colors.subList(3,7)
|
||||
for((name, color, onColor) in section){
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = color),
|
||||
onClick = {}) {
|
||||
Text(name, color = onColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val section = colors.subList(7,11)
|
||||
for((name, color, onColor) in section){
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = color),
|
||||
onClick = {}) {
|
||||
Text(name, color = onColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val section = colors.subList(11, 16)
|
||||
for ((name, color, onColor) in section) {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = color),
|
||||
onClick = {}) {
|
||||
Text(name, color = onColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ComponentPreview("Text & Fonts") {
|
||||
Column {
|
||||
Text("displayLarge", style = MaterialTheme.typography.displayLarge)
|
||||
Text("displayMedium", style = MaterialTheme.typography.displayMedium)
|
||||
Text("displaySmall", style = MaterialTheme.typography.displaySmall)
|
||||
|
||||
Text("headlineLarge", style = MaterialTheme.typography.headlineLarge)
|
||||
Text("headlineMedium", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("headlineSmall", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
Text("titleLarge", style = MaterialTheme.typography.titleLarge)
|
||||
Text("titleMedium", style = MaterialTheme.typography.titleMedium)
|
||||
Text("titleSmall", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
Text("bodyLarge", style = MaterialTheme.typography.bodyLarge)
|
||||
Text("bodyMedium", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("bodySmall", style = MaterialTheme.typography.bodySmall)
|
||||
|
||||
Text("labelLarge", style = MaterialTheme.typography.labelLarge)
|
||||
Text("labelMedium", style = MaterialTheme.typography.labelMedium)
|
||||
Text("labelSmall", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
ComponentPreview("Buttons") {
|
||||
Button(onClick = {}) {
|
||||
Text("Filled")
|
||||
}
|
||||
Button(onClick = {}, enabled = false) {
|
||||
Text("Disabled")
|
||||
}
|
||||
OutlinedButton(onClick = {}) {
|
||||
Text("Outlined")
|
||||
}
|
||||
TextButton(onClick = {}) {
|
||||
Text("Text")
|
||||
}
|
||||
}
|
||||
ComponentPreview("Icon Buttons") {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(Icons.Default.Map, contentDescription = "Icon Button")
|
||||
}
|
||||
}
|
||||
ComponentPreview("Chip") {
|
||||
AssistChip(onClick = {}, label = {
|
||||
Text("Assist Chip")
|
||||
})
|
||||
FilterChip(selected = false, onClick = {}, label = {
|
||||
Text("Filter not Selected")
|
||||
})
|
||||
FilterChip(selected = true, onClick = {}, label = {
|
||||
Text("Filter Selected")
|
||||
})
|
||||
}
|
||||
ComponentPreview("Progress Indicator") {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)){
|
||||
CircularProgressIndicator()
|
||||
LinearProgressIndicator()
|
||||
}
|
||||
}
|
||||
ComponentPreview("Radio Button") {
|
||||
var state by remember { mutableStateOf(true) }
|
||||
RadioButton(!state, onClick = { state = false })
|
||||
RadioButton(state, onClick = { state = true })
|
||||
|
||||
}
|
||||
ComponentPreview("Checkbox") {
|
||||
var state by remember { mutableStateOf(true) }
|
||||
Checkbox(state, onCheckedChange = { state = it })
|
||||
Checkbox(!state, onCheckedChange = { state = !it })
|
||||
Checkbox(state, onCheckedChange = {}, enabled = false)
|
||||
TriStateCheckbox(ToggleableState.Indeterminate, onClick = {})
|
||||
}
|
||||
ComponentPreview("Switch") {
|
||||
var state by remember { mutableStateOf(true) }
|
||||
Switch(state, onCheckedChange = { state = it })
|
||||
Switch(!state, enabled = false, onCheckedChange = { state = it })
|
||||
}
|
||||
ComponentPreview("Slider") {
|
||||
Column{
|
||||
var state by remember { mutableStateOf(0.5f) }
|
||||
Slider(state, onValueChange = { state = it })
|
||||
var rangeState by remember { mutableStateOf(0.25f..0.75f) }
|
||||
RangeSlider(rangeState, onValueChange = { rangeState = it })
|
||||
}
|
||||
|
||||
}
|
||||
ComponentPreview("Badge") {
|
||||
IconButton(onClick = {}) {
|
||||
BadgedBox(badge = { Badge() }) {
|
||||
Icon(Icons.Default.Map, contentDescription = "Icon with Badge")
|
||||
}
|
||||
}
|
||||
}
|
||||
ComponentPreview("Number Field") {
|
||||
var number by remember { mutableStateOf("123") }
|
||||
TextField(number, onValueChange = {
|
||||
if(it.all { char -> char.isDigit() }) {
|
||||
number = it
|
||||
}
|
||||
}, label = { Text("Number Field") })
|
||||
|
||||
}
|
||||
ComponentPreview("Text Field") {
|
||||
Row {
|
||||
var text by remember { mutableStateOf("Text Field") }
|
||||
TextField(text, onValueChange = { text = it })
|
||||
}
|
||||
var text by remember { mutableStateOf("Outlined Text Field") }
|
||||
OutlinedTextField(text, onValueChange = { text = it})
|
||||
}
|
||||
ComponentPreview("Dropdown Menu") {
|
||||
var show by remember { mutableStateOf(false) }
|
||||
AssistChip(
|
||||
onClick = { show = true },
|
||||
label = { Text("Show Menu") }
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = show,
|
||||
onDismissRequest = {
|
||||
show = false
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(onClick = { show = false }, text = {
|
||||
Text("Menu Item 1", modifier = Modifier.padding(8.dp))
|
||||
})
|
||||
DropdownMenuItem(onClick = { show = false }, text = {
|
||||
Text("Menu Item 2", modifier = Modifier.padding(8.dp))
|
||||
})
|
||||
DropdownMenuItem(onClick = { show = false }, text = {
|
||||
Text("Menu Item 3", modifier = Modifier.padding(8.dp))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
ComponentPreview("Card") {
|
||||
Card{
|
||||
Text("Hello, Tabs!", modifier = Modifier.padding(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
ComponentPreview("Scrollable View") {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
ComponentPreview("Tabs") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return color.toInt()
|
||||
}
|
||||
throw IllegalArgumentException("Unknown color")
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(title: String, content: @Composable () -> Unit) {
|
||||
Column {
|
||||
Text(title, style = MaterialTheme.typography.titleLarge)
|
||||
HorizontalDivider()
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
content()
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package processing.app.ui.theme
|
||||
|
||||
import androidx.compose.material.MaterialTheme.typography
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -21,18 +20,108 @@ 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(
|
||||
body1 = TextStyle(
|
||||
fontFamily = processingFont,
|
||||
@Deprecated("Use PDE3Typography instead")
|
||||
val PDE2Typography = Typography(
|
||||
defaultFontFamily = spaceGroteskFont,
|
||||
h1 = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 42.725.sp,
|
||||
lineHeight = 48.sp
|
||||
),
|
||||
h2 = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 34.18.sp,
|
||||
lineHeight = 40.sp
|
||||
),
|
||||
h3 = TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 27.344.sp,
|
||||
lineHeight = 32.sp
|
||||
),
|
||||
h4 = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 13.sp,
|
||||
fontSize = 21.875.sp,
|
||||
lineHeight = 28.sp
|
||||
),
|
||||
h5 = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 17.5.sp,
|
||||
lineHeight = 22.sp
|
||||
),
|
||||
h6 = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
),
|
||||
body1 = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
),
|
||||
body2 = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.8.sp,
|
||||
lineHeight = 16.sp
|
||||
),
|
||||
subtitle1 = TextStyle(
|
||||
fontFamily = processingFont,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
subtitle2 = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 13.824.sp,
|
||||
lineHeight = 16.sp,
|
||||
),
|
||||
caption = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 11.2.sp,
|
||||
lineHeight = 14.sp
|
||||
),
|
||||
overline = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 8.96.sp,
|
||||
lineHeight = 10.sp
|
||||
)
|
||||
)
|
||||
val base = androidx.compose.material3.Typography()
|
||||
val PDETypography = androidx.compose.material3.Typography(
|
||||
displayLarge = base.displayLarge.copy(fontFamily = spaceGroteskFont),
|
||||
displayMedium = base.displayMedium.copy(fontFamily = spaceGroteskFont),
|
||||
displaySmall = base.displaySmall.copy(fontFamily = spaceGroteskFont),
|
||||
headlineLarge = base.headlineLarge.copy(fontFamily = spaceGroteskFont),
|
||||
headlineMedium = base.headlineMedium.copy(fontFamily = spaceGroteskFont),
|
||||
headlineSmall = base.headlineSmall.copy(fontFamily = spaceGroteskFont),
|
||||
titleLarge = base.titleLarge.copy(fontFamily = spaceGroteskFont),
|
||||
titleMedium = base.titleMedium.copy(fontFamily = spaceGroteskFont),
|
||||
titleSmall = base.titleSmall.copy(fontFamily = spaceGroteskFont),
|
||||
bodyLarge = base.bodyLarge.copy(fontFamily = spaceGroteskFont),
|
||||
bodyMedium = base.bodyMedium.copy(fontFamily = spaceGroteskFont),
|
||||
bodySmall = base.bodySmall.copy(fontFamily = spaceGroteskFont),
|
||||
labelLarge = base.labelLarge.copy(fontFamily = spaceGroteskFont),
|
||||
labelMedium = base.labelMedium.copy(fontFamily = spaceGroteskFont),
|
||||
labelSmall = base.labelSmall.copy(fontFamily = spaceGroteskFont),
|
||||
)
|
||||
238
app/src/processing/app/ui/theme/Window.kt
Normal file
238
app/src/processing/app/ui/theme/Window.kt
Normal file
@@ -0,0 +1,238 @@
|
||||
package processing.app.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.awt.ComposeWindow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 processing.app.ui.Toolkit
|
||||
import java.awt.Dimension
|
||||
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JRootPane
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val LocalWindow = compositionLocalOf<JFrame> { error("No Window Set") }
|
||||
|
||||
/**
|
||||
* A utility class to create a new Window with Compose content in a Swing application.
|
||||
* It sets up the window with some default properties and allows for custom content.
|
||||
* Use this when creating a Compose based window from Swing.
|
||||
*
|
||||
* Usage example:
|
||||
* ```
|
||||
* SwingUtilities.invokeLater {
|
||||
* PDESwingWindow("menu.help.welcome", fullWindowContent = true) {
|
||||
*
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param titleKey The key for the window title, which will be localized.
|
||||
* @param size The desired size of the window. If null, the window will use its default size.
|
||||
* @param minSize The minimum size of the window. If null, no minimum size is set.
|
||||
* @param maxSize The maximum size of the window. If null, no maximum size is set.
|
||||
* @param unique An optional unique identifier for the window to prevent duplicates.
|
||||
* @param onClose A lambda function to be called when the window is requested to close.
|
||||
* @param fullWindowContent If true, the content will extend into the title bar area on macOS.
|
||||
* @param content The composable content to be displayed in the window.
|
||||
*/
|
||||
class PDESwingWindow(
|
||||
titleKey: String = "",
|
||||
size: Dimension? = null,
|
||||
minSize: Dimension? = null,
|
||||
maxSize: Dimension? = null,
|
||||
unique: KClass<*>? = null,
|
||||
fullWindowContent: Boolean = false,
|
||||
onClose: () -> Unit = {},
|
||||
content: @Composable () -> Unit
|
||||
){
|
||||
init{
|
||||
ComposeWindow().apply {
|
||||
val window = this
|
||||
defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
|
||||
size?.let {
|
||||
window.size = it
|
||||
}
|
||||
minSize?.let {
|
||||
window.minimumSize = it
|
||||
}
|
||||
maxSize?.let {
|
||||
window.maximumSize = it
|
||||
}
|
||||
setLocationRelativeTo(null)
|
||||
setContent {
|
||||
PDEWindowContent(
|
||||
window = window,
|
||||
titleKey = titleKey,
|
||||
unique = unique,
|
||||
fullWindowContent = fullWindowContent,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
window.addWindowStateListener {
|
||||
if(it.newState == JFrame.DISPOSE_ON_CLOSE){
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val windows = mutableMapOf<KClass<*>, ComposeWindow>()
|
||||
|
||||
/**
|
||||
* Internal Composable function to set up the window content with theming and localization.
|
||||
* It also handles macOS specific properties for full window content.
|
||||
*
|
||||
* @param window The JFrame instance to be configured.
|
||||
* @param titleKey The key for the window title, which will be localized.
|
||||
* @param unique An optional unique identifier for the window to prevent duplicates.
|
||||
* @param fullWindowContent If true, the content will extend into the title bar area on macOS.
|
||||
* @param content The composable content to be displayed in the window.
|
||||
*/
|
||||
@Composable
|
||||
private fun PDEWindowContent(
|
||||
window: ComposeWindow,
|
||||
titleKey: String,
|
||||
unique: KClass<*>? = null,
|
||||
fullWindowContent: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
){
|
||||
val mac = SystemInfo.isMacOS && SystemInfo.isMacFullWindowContentSupported
|
||||
remember {
|
||||
window.rootPane.putClientProperty("apple.awt.fullWindowContent", mac && fullWindowContent)
|
||||
window.rootPane.putClientProperty("apple.awt.transparentTitleBar", mac && fullWindowContent)
|
||||
Toolkit.setIcon(window)
|
||||
}
|
||||
if(unique != null && windows.contains(unique) && windows[unique] != null){
|
||||
windows[unique]?.toFront()
|
||||
window.dispose()
|
||||
return
|
||||
}
|
||||
|
||||
DisposableEffect(unique){
|
||||
unique?.let {
|
||||
windows[it] = window
|
||||
}
|
||||
onDispose {
|
||||
windows.remove(unique)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalWindow provides window) {
|
||||
PDETheme{
|
||||
val locale = LocalLocale.current
|
||||
window.title = locale[titleKey]
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable function to create and display a new window with the specified content.
|
||||
* This function sets up the window state and handles the close request.
|
||||
* Use this when creating a Compose based window from another Compose context.
|
||||
*
|
||||
* Usage example:
|
||||
* ```
|
||||
* PDEComposeWindow("window.title", fullWindowContent = true, onClose = { /* handle close */ }) {
|
||||
* // Your window content here
|
||||
* Text("Hello, World!")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This will create a new window with the title localized from "window.title" key,
|
||||
* with content extending into the title bar area on macOS, and a custom close handler.
|
||||
*
|
||||
* Fully standalone example:
|
||||
* ```
|
||||
* application {
|
||||
* PDEComposeWindow("window.title", fullWindowContent = true, onClose = ::exitApplication) {
|
||||
* // Your window content here
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param titleKey The key for the window title, which will be localized.
|
||||
* @param size The desired size of the window. Defaults to unspecified size which means the window will be
|
||||
* fullscreen if it contains any of [fillMaxWidth]/[fillMaxSize]/[fillMaxHeight] etc.
|
||||
* @param minSize The minimum size of the window. Defaults to unspecified size which means no minimum size is set.
|
||||
* @param maxSize The maximum size of the window. Defaults to unspecified size which means no maximum size is set.
|
||||
* @param unique An optional unique identifier for the window to prevent duplicates.
|
||||
* @param fullWindowContent If true, the content will extend into the title bar area on macOS.
|
||||
* @param onClose A lambda function to be called when the window is requested to close.
|
||||
* @param content The composable content to be displayed in the window.
|
||||
*/
|
||||
@Composable
|
||||
fun PDEComposeWindow(
|
||||
titleKey: String,
|
||||
size: DpSize = DpSize.Unspecified,
|
||||
minSize: DpSize = DpSize.Unspecified,
|
||||
maxSize: DpSize = DpSize.Unspecified,
|
||||
unique: KClass<*>? = null,
|
||||
fullWindowContent: Boolean = false,
|
||||
onClose: () -> Unit = {},
|
||||
content: @Composable () -> Unit
|
||||
){
|
||||
val windowState = rememberWindowState(
|
||||
size = size,
|
||||
position = WindowPosition(Alignment.Center)
|
||||
)
|
||||
Window(onCloseRequest = onClose, state = windowState, title = "") {
|
||||
remember {
|
||||
window.minimumSize = minSize.toDimension()
|
||||
window.maximumSize = maxSize.toDimension()
|
||||
}
|
||||
PDEWindowContent(
|
||||
window = window,
|
||||
titleKey = titleKey,
|
||||
unique = unique,
|
||||
fullWindowContent = fullWindowContent,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun DpSize.toDimension(): Dimension? {
|
||||
if(this == DpSize.Unspecified) { return null }
|
||||
|
||||
return Dimension(
|
||||
this.width.value.toInt(),
|
||||
this.height.value.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
fun main(){
|
||||
application {
|
||||
PDEComposeWindow(
|
||||
onClose = ::exitApplication,
|
||||
titleKey = "window.title",
|
||||
size = DpSize(800.dp, 600.dp),
|
||||
){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Hello, World!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
app/src/processing/app/ui/theme/m3/Color.kt
Normal file
252
app/src/processing/app/ui/theme/m3/Color.kt
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* This file was generated by the Material Theme Builder tool.
|
||||
* Do not edit this file directly.
|
||||
*/
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val primaryLight = Color(0xFF525A92)
|
||||
val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
val primaryContainerLight = Color(0xFF293DAE)
|
||||
val onPrimaryContainerLight = Color(0xFFABB5FF)
|
||||
val secondaryLight = Color(0xFF555D7D)
|
||||
val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLight = Color(0xFF8890B3)
|
||||
val onSecondaryContainerLight = Color(0xFF212946)
|
||||
val tertiaryLight = Color(0xFF0052CC)
|
||||
val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLight = Color(0xFF0468FF)
|
||||
val onTertiaryContainerLight = Color(0xFFFBF9FF)
|
||||
val errorLight = Color(0xFFBB0026)
|
||||
val onErrorLight = Color(0xFFFFFFFF)
|
||||
val errorContainerLight = Color(0xFFE41D37)
|
||||
val onErrorContainerLight = Color(0xFFFFFBFF)
|
||||
val backgroundLight = Color(0xFFFBF8FF)
|
||||
val onBackgroundLight = Color(0xFF1A1B22)
|
||||
val surfaceLight = Color(0xFFFDF8F8)
|
||||
val onSurfaceLight = Color(0xFF1C1B1C)
|
||||
val surfaceVariantLight = Color(0xFFE4E1E8)
|
||||
val onSurfaceVariantLight = Color(0xFF47464B)
|
||||
val outlineLight = Color(0xFF77767C)
|
||||
val outlineVariantLight = Color(0xFFC8C5CB)
|
||||
val scrimLight = Color(0xFF000000)
|
||||
val inverseSurfaceLight = Color(0xFF313030)
|
||||
val inverseOnSurfaceLight = Color(0xFFF4F0EF)
|
||||
val inversePrimaryLight = Color(0xFFBBC3FF)
|
||||
val surfaceDimLight = Color(0xFFDDD9D9)
|
||||
val surfaceBrightLight = Color(0xFFFDF8F8)
|
||||
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLight = Color(0xFFF7F3F2)
|
||||
val surfaceContainerLight = Color(0xFFF1EDED)
|
||||
val surfaceContainerHighLight = Color(0xFFEBE7E7)
|
||||
val surfaceContainerHighestLight = Color(0xFFE5E2E1)
|
||||
|
||||
val primaryLightMediumContrast = Color(0xFF525A92)
|
||||
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightMediumContrast = Color(0xFF293DAE)
|
||||
val onPrimaryContainerLightMediumContrast = Color(0xFFE3E4FF)
|
||||
val secondaryLightMediumContrast = Color(0xFF2D3553)
|
||||
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightMediumContrast = Color(0xFF646C8D)
|
||||
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightMediumContrast = Color(0xFF003080)
|
||||
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightMediumContrast = Color(0xFF0062F3)
|
||||
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorLightMediumContrast = Color(0xFF730013)
|
||||
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightMediumContrast = Color(0xFFD91030)
|
||||
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightMediumContrast = Color(0xFFFBF8FF)
|
||||
val onBackgroundLightMediumContrast = Color(0xFF1A1B22)
|
||||
val surfaceLightMediumContrast = Color(0xFFFDF8F8)
|
||||
val onSurfaceLightMediumContrast = Color(0xFF111111)
|
||||
val surfaceVariantLightMediumContrast = Color(0xFFE4E1E8)
|
||||
val onSurfaceVariantLightMediumContrast = Color(0xFF36363B)
|
||||
val outlineLightMediumContrast = Color(0xFF525257)
|
||||
val outlineVariantLightMediumContrast = Color(0xFF6D6C72)
|
||||
val scrimLightMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightMediumContrast = Color(0xFF313030)
|
||||
val inverseOnSurfaceLightMediumContrast = Color(0xFFF4F0EF)
|
||||
val inversePrimaryLightMediumContrast = Color(0xFFBBC3FF)
|
||||
val surfaceDimLightMediumContrast = Color(0xFFC9C6C5)
|
||||
val surfaceBrightLightMediumContrast = Color(0xFFFDF8F8)
|
||||
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightMediumContrast = Color(0xFFF7F3F2)
|
||||
val surfaceContainerLightMediumContrast = Color(0xFFEBE7E7)
|
||||
val surfaceContainerHighLightMediumContrast = Color(0xFFE0DCDC)
|
||||
val surfaceContainerHighestLightMediumContrast = Color(0xFFD4D1D0)
|
||||
|
||||
val primaryLightHighContrast = Color(0xFF525A92)
|
||||
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightHighContrast = Color(0xFF283CAD)
|
||||
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightHighContrast = Color(0xFF222B48)
|
||||
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightHighContrast = Color(0xFF404867)
|
||||
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightHighContrast = Color(0xFF00276B)
|
||||
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightHighContrast = Color(0xFF0042A8)
|
||||
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorLightHighContrast = Color(0xFF60000E)
|
||||
val onErrorLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightHighContrast = Color(0xFF97001C)
|
||||
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightHighContrast = Color(0xFFFBF8FF)
|
||||
val onBackgroundLightHighContrast = Color(0xFF1A1B22)
|
||||
val surfaceLightHighContrast = Color(0xFFFDF8F8)
|
||||
val onSurfaceLightHighContrast = Color(0xFF000000)
|
||||
val surfaceVariantLightHighContrast = Color(0xFFE4E1E8)
|
||||
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
|
||||
val outlineLightHighContrast = Color(0xFF2C2C30)
|
||||
val outlineVariantLightHighContrast = Color(0xFF49494E)
|
||||
val scrimLightHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightHighContrast = Color(0xFF313030)
|
||||
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
|
||||
val inversePrimaryLightHighContrast = Color(0xFFBBC3FF)
|
||||
val surfaceDimLightHighContrast = Color(0xFFBBB8B8)
|
||||
val surfaceBrightLightHighContrast = Color(0xFFFDF8F8)
|
||||
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightHighContrast = Color(0xFFF4F0EF)
|
||||
val surfaceContainerLightHighContrast = Color(0xFFE5E2E1)
|
||||
val surfaceContainerHighLightHighContrast = Color(0xFFD7D3D3)
|
||||
val surfaceContainerHighestLightHighContrast = Color(0xFFC9C6C5)
|
||||
|
||||
val primaryDark = Color(0xFFBBC3FF)
|
||||
val onPrimaryDark = Color(0xFF001D93)
|
||||
val primaryContainerDark = Color(0xFF293DAE)
|
||||
val onPrimaryContainerDark = Color(0xFFABB5FF)
|
||||
val secondaryDark = Color(0xFFBDC5EA)
|
||||
val onSecondaryDark = Color(0xFF272F4D)
|
||||
val secondaryContainerDark = Color(0xFF8890B3)
|
||||
val onSecondaryContainerDark = Color(0xFF212946)
|
||||
val tertiaryDark = Color(0xFFB2C5FF)
|
||||
val onTertiaryDark = Color(0xFF002B74)
|
||||
val tertiaryContainerDark = Color(0xFF0468FF)
|
||||
val onTertiaryContainerDark = Color(0xFFFBF9FF)
|
||||
val errorDark = Color(0xFFFFB3B0)
|
||||
val onErrorDark = Color(0xFF680010)
|
||||
val errorContainerDark = Color(0xFFFF5359)
|
||||
val onErrorContainerDark = Color(0xFF220002)
|
||||
val backgroundDark = Color(0xFF12131A)
|
||||
val onBackgroundDark = Color(0xFFE3E1EB)
|
||||
val surfaceDark = Color(0xFF141313)
|
||||
val onSurfaceDark = Color(0xFFE5E2E1)
|
||||
val surfaceVariantDark = Color(0xFF47464B)
|
||||
val onSurfaceVariantDark = Color(0xFFC8C5CB)
|
||||
val outlineDark = Color(0xFF919096)
|
||||
val outlineVariantDark = Color(0xFF47464B)
|
||||
val scrimDark = Color(0xFF000000)
|
||||
val inverseSurfaceDark = Color(0xFFE5E2E1)
|
||||
val inverseOnSurfaceDark = Color(0xFF313030)
|
||||
val inversePrimaryDark = Color(0xFF4053C3)
|
||||
val surfaceDimDark = Color(0xFF141313)
|
||||
val surfaceBrightDark = Color(0xFF3A3939)
|
||||
val surfaceContainerLowestDark = Color(0xFF0E0E0E)
|
||||
val surfaceContainerLowDark = Color(0xFF1C1B1C)
|
||||
val surfaceContainerDark = Color(0xFF201F20)
|
||||
val surfaceContainerHighDark = Color(0xFF2B2A2A)
|
||||
val surfaceContainerHighestDark = Color(0xFF353435)
|
||||
|
||||
val primaryDarkMediumContrast = Color(0xFFBBC3FF)
|
||||
val onPrimaryDarkMediumContrast = Color(0xFF001677)
|
||||
val primaryContainerDarkMediumContrast = Color(0xFF7587FA)
|
||||
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val secondaryDarkMediumContrast = Color(0xFFD4DBFF)
|
||||
val onSecondaryDarkMediumContrast = Color(0xFF1C2441)
|
||||
val secondaryContainerDarkMediumContrast = Color(0xFF8890B3)
|
||||
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val tertiaryDarkMediumContrast = Color(0xFFD2DBFF)
|
||||
val onTertiaryDarkMediumContrast = Color(0xFF00215E)
|
||||
val tertiaryContainerDarkMediumContrast = Color(0xFF5D8BFF)
|
||||
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val errorDarkMediumContrast = Color(0xFFFFD2CF)
|
||||
val onErrorDarkMediumContrast = Color(0xFF54000B)
|
||||
val errorContainerDarkMediumContrast = Color(0xFFFF5359)
|
||||
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val backgroundDarkMediumContrast = Color(0xFF12131A)
|
||||
val onBackgroundDarkMediumContrast = Color(0xFFE3E1EB)
|
||||
val surfaceDarkMediumContrast = Color(0xFF141313)
|
||||
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkMediumContrast = Color(0xFF47464B)
|
||||
val onSurfaceVariantDarkMediumContrast = Color(0xFFDEDBE1)
|
||||
val outlineDarkMediumContrast = Color(0xFFB3B1B7)
|
||||
val outlineVariantDarkMediumContrast = Color(0xFF918F95)
|
||||
val scrimDarkMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkMediumContrast = Color(0xFFE5E2E1)
|
||||
val inverseOnSurfaceDarkMediumContrast = Color(0xFF2B2A2A)
|
||||
val inversePrimaryDarkMediumContrast = Color(0xFF263AAC)
|
||||
val surfaceDimDarkMediumContrast = Color(0xFF141313)
|
||||
val surfaceBrightDarkMediumContrast = Color(0xFF454444)
|
||||
val surfaceContainerLowestDarkMediumContrast = Color(0xFF080707)
|
||||
val surfaceContainerLowDarkMediumContrast = Color(0xFF1E1D1E)
|
||||
val surfaceContainerDarkMediumContrast = Color(0xFF282828)
|
||||
val surfaceContainerHighDarkMediumContrast = Color(0xFF333232)
|
||||
val surfaceContainerHighestDarkMediumContrast = Color(0xFF3E3D3D)
|
||||
|
||||
val primaryDarkHighContrast = Color(0xFFBBC3FF)
|
||||
val onPrimaryDarkHighContrast = Color(0xFF000000)
|
||||
val primaryContainerDarkHighContrast = Color(0xFFB6BFFF)
|
||||
val onPrimaryContainerDarkHighContrast = Color(0xFF000533)
|
||||
val secondaryDarkHighContrast = Color(0xFFEEEFFF)
|
||||
val onSecondaryDarkHighContrast = Color(0xFF000000)
|
||||
val secondaryContainerDarkHighContrast = Color(0xFFB9C1E6)
|
||||
val onSecondaryContainerDarkHighContrast = Color(0xFF020926)
|
||||
val tertiaryDarkHighContrast = Color(0xFFEDEFFF)
|
||||
val onTertiaryDarkHighContrast = Color(0xFF000000)
|
||||
val tertiaryContainerDarkHighContrast = Color(0xFFADC1FF)
|
||||
val onTertiaryContainerDarkHighContrast = Color(0xFF000926)
|
||||
val errorDarkHighContrast = Color(0xFFFFECEA)
|
||||
val onErrorDarkHighContrast = Color(0xFF000000)
|
||||
val errorContainerDarkHighContrast = Color(0xFFFFADAB)
|
||||
val onErrorContainerDarkHighContrast = Color(0xFF220002)
|
||||
val backgroundDarkHighContrast = Color(0xFF12131A)
|
||||
val onBackgroundDarkHighContrast = Color(0xFFE3E1EB)
|
||||
val surfaceDarkHighContrast = Color(0xFF141313)
|
||||
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkHighContrast = Color(0xFF47464B)
|
||||
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val outlineDarkHighContrast = Color(0xFFF2EFF5)
|
||||
val outlineVariantDarkHighContrast = Color(0xFFC4C2C8)
|
||||
val scrimDarkHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkHighContrast = Color(0xFFE5E2E1)
|
||||
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
|
||||
val inversePrimaryDarkHighContrast = Color(0xFF263AAC)
|
||||
val surfaceDimDarkHighContrast = Color(0xFF141313)
|
||||
val surfaceBrightDarkHighContrast = Color(0xFF515050)
|
||||
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
|
||||
val surfaceContainerLowDarkHighContrast = Color(0xFF201F20)
|
||||
val surfaceContainerDarkHighContrast = Color(0xFF313030)
|
||||
val surfaceContainerHighDarkHighContrast = Color(0xFF3C3B3B)
|
||||
val surfaceContainerHighestDarkHighContrast = Color(0xFF484646)
|
||||
|
||||
val warningLight = Color(0xFF765B0B)
|
||||
val onWarningLight = Color(0xFFFFFFFF)
|
||||
val warningContainerLight = Color(0xFFFFDF97)
|
||||
val onWarningContainerLight = Color(0xFF5A4300)
|
||||
|
||||
val warningLightMediumContrast = Color(0xFF453400)
|
||||
val onWarningLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val warningContainerLightMediumContrast = Color(0xFF86691C)
|
||||
val onWarningContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
|
||||
val warningLightHighContrast = Color(0xFF392A00)
|
||||
val onWarningLightHighContrast = Color(0xFFFFFFFF)
|
||||
val warningContainerLightHighContrast = Color(0xFF5D4600)
|
||||
val onWarningContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
|
||||
val warningDark = Color(0xFFE6C26C)
|
||||
val onWarningDark = Color(0xFF3E2E00)
|
||||
val warningContainerDark = Color(0xFF5A4300)
|
||||
val onWarningContainerDark = Color(0xFFFFDF97)
|
||||
|
||||
val warningDarkMediumContrast = Color(0xFFFED87F)
|
||||
val onWarningDarkMediumContrast = Color(0xFF312400)
|
||||
val warningContainerDarkMediumContrast = Color(0xFFAD8D3D)
|
||||
val onWarningContainerDarkMediumContrast = Color(0xFF000000)
|
||||
|
||||
val warningDarkHighContrast = Color(0xFFFFEECF)
|
||||
val onWarningDarkHighContrast = Color(0xFF000000)
|
||||
val warningContainerDarkHighContrast = Color(0xFFE2BE69)
|
||||
val onWarningContainerDarkHighContrast = Color(0xFF110A00)
|
||||
|
||||
304
app/src/processing/app/ui/theme/m3/Theme.kt
Normal file
304
app/src/processing/app/ui/theme/m3/Theme.kt
Normal file
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* This file was generated by the Material Theme Builder tool.
|
||||
* Do not edit this file directly.
|
||||
*/
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Immutable
|
||||
data class ExtendedColorScheme(
|
||||
val warning: ColorFamily,
|
||||
)
|
||||
|
||||
val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
primaryContainer = primaryContainerLight,
|
||||
onPrimaryContainer = onPrimaryContainerLight,
|
||||
secondary = secondaryLight,
|
||||
onSecondary = onSecondaryLight,
|
||||
secondaryContainer = secondaryContainerLight,
|
||||
onSecondaryContainer = onSecondaryContainerLight,
|
||||
tertiary = tertiaryLight,
|
||||
onTertiary = onTertiaryLight,
|
||||
tertiaryContainer = tertiaryContainerLight,
|
||||
onTertiaryContainer = onTertiaryContainerLight,
|
||||
error = errorLight,
|
||||
onError = onErrorLight,
|
||||
errorContainer = errorContainerLight,
|
||||
onErrorContainer = onErrorContainerLight,
|
||||
background = backgroundLight,
|
||||
onBackground = onBackgroundLight,
|
||||
surface = surfaceLight,
|
||||
onSurface = onSurfaceLight,
|
||||
surfaceVariant = surfaceVariantLight,
|
||||
onSurfaceVariant = onSurfaceVariantLight,
|
||||
outline = outlineLight,
|
||||
outlineVariant = outlineVariantLight,
|
||||
scrim = scrimLight,
|
||||
inverseSurface = inverseSurfaceLight,
|
||||
inverseOnSurface = inverseOnSurfaceLight,
|
||||
inversePrimary = inversePrimaryLight,
|
||||
surfaceDim = surfaceDimLight,
|
||||
surfaceBright = surfaceBrightLight,
|
||||
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||
surfaceContainerLow = surfaceContainerLowLight,
|
||||
surfaceContainer = surfaceContainerLight,
|
||||
surfaceContainerHigh = surfaceContainerHighLight,
|
||||
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||
)
|
||||
|
||||
val darkScheme = darkColorScheme(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
primaryContainer = primaryContainerDark,
|
||||
onPrimaryContainer = onPrimaryContainerDark,
|
||||
secondary = secondaryDark,
|
||||
onSecondary = onSecondaryDark,
|
||||
secondaryContainer = secondaryContainerDark,
|
||||
onSecondaryContainer = onSecondaryContainerDark,
|
||||
tertiary = tertiaryDark,
|
||||
onTertiary = onTertiaryDark,
|
||||
tertiaryContainer = tertiaryContainerDark,
|
||||
onTertiaryContainer = onTertiaryContainerDark,
|
||||
error = errorDark,
|
||||
onError = onErrorDark,
|
||||
errorContainer = errorContainerDark,
|
||||
onErrorContainer = onErrorContainerDark,
|
||||
background = backgroundDark,
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
onSurfaceVariant = onSurfaceVariantDark,
|
||||
outline = outlineDark,
|
||||
outlineVariant = outlineVariantDark,
|
||||
scrim = scrimDark,
|
||||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
surfaceDim = surfaceDimDark,
|
||||
surfaceBright = surfaceBrightDark,
|
||||
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||
surfaceContainerLow = surfaceContainerLowDark,
|
||||
surfaceContainer = surfaceContainerDark,
|
||||
surfaceContainerHigh = surfaceContainerHighDark,
|
||||
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
private val mediumContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightMediumContrast,
|
||||
onPrimary = onPrimaryLightMediumContrast,
|
||||
primaryContainer = primaryContainerLightMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
|
||||
secondary = secondaryLightMediumContrast,
|
||||
onSecondary = onSecondaryLightMediumContrast,
|
||||
secondaryContainer = secondaryContainerLightMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
|
||||
tertiary = tertiaryLightMediumContrast,
|
||||
onTertiary = onTertiaryLightMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerLightMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
|
||||
error = errorLightMediumContrast,
|
||||
onError = onErrorLightMediumContrast,
|
||||
errorContainer = errorContainerLightMediumContrast,
|
||||
onErrorContainer = onErrorContainerLightMediumContrast,
|
||||
background = backgroundLightMediumContrast,
|
||||
onBackground = onBackgroundLightMediumContrast,
|
||||
surface = surfaceLightMediumContrast,
|
||||
onSurface = onSurfaceLightMediumContrast,
|
||||
surfaceVariant = surfaceVariantLightMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
|
||||
outline = outlineLightMediumContrast,
|
||||
outlineVariant = outlineVariantLightMediumContrast,
|
||||
scrim = scrimLightMediumContrast,
|
||||
inverseSurface = inverseSurfaceLightMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
|
||||
inversePrimary = inversePrimaryLightMediumContrast,
|
||||
surfaceDim = surfaceDimLightMediumContrast,
|
||||
surfaceBright = surfaceBrightLightMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
|
||||
surfaceContainer = surfaceContainerLightMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightHighContrast,
|
||||
onPrimary = onPrimaryLightHighContrast,
|
||||
primaryContainer = primaryContainerLightHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightHighContrast,
|
||||
secondary = secondaryLightHighContrast,
|
||||
onSecondary = onSecondaryLightHighContrast,
|
||||
secondaryContainer = secondaryContainerLightHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightHighContrast,
|
||||
tertiary = tertiaryLightHighContrast,
|
||||
onTertiary = onTertiaryLightHighContrast,
|
||||
tertiaryContainer = tertiaryContainerLightHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightHighContrast,
|
||||
error = errorLightHighContrast,
|
||||
onError = onErrorLightHighContrast,
|
||||
errorContainer = errorContainerLightHighContrast,
|
||||
onErrorContainer = onErrorContainerLightHighContrast,
|
||||
background = backgroundLightHighContrast,
|
||||
onBackground = onBackgroundLightHighContrast,
|
||||
surface = surfaceLightHighContrast,
|
||||
onSurface = onSurfaceLightHighContrast,
|
||||
surfaceVariant = surfaceVariantLightHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightHighContrast,
|
||||
outline = outlineLightHighContrast,
|
||||
outlineVariant = outlineVariantLightHighContrast,
|
||||
scrim = scrimLightHighContrast,
|
||||
inverseSurface = inverseSurfaceLightHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightHighContrast,
|
||||
inversePrimary = inversePrimaryLightHighContrast,
|
||||
surfaceDim = surfaceDimLightHighContrast,
|
||||
surfaceBright = surfaceBrightLightHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightHighContrast,
|
||||
surfaceContainer = surfaceContainerLightHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
|
||||
)
|
||||
|
||||
private val mediumContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkMediumContrast,
|
||||
onPrimary = onPrimaryDarkMediumContrast,
|
||||
primaryContainer = primaryContainerDarkMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
|
||||
secondary = secondaryDarkMediumContrast,
|
||||
onSecondary = onSecondaryDarkMediumContrast,
|
||||
secondaryContainer = secondaryContainerDarkMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
|
||||
tertiary = tertiaryDarkMediumContrast,
|
||||
onTertiary = onTertiaryDarkMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
|
||||
error = errorDarkMediumContrast,
|
||||
onError = onErrorDarkMediumContrast,
|
||||
errorContainer = errorContainerDarkMediumContrast,
|
||||
onErrorContainer = onErrorContainerDarkMediumContrast,
|
||||
background = backgroundDarkMediumContrast,
|
||||
onBackground = onBackgroundDarkMediumContrast,
|
||||
surface = surfaceDarkMediumContrast,
|
||||
onSurface = onSurfaceDarkMediumContrast,
|
||||
surfaceVariant = surfaceVariantDarkMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
|
||||
outline = outlineDarkMediumContrast,
|
||||
outlineVariant = outlineVariantDarkMediumContrast,
|
||||
scrim = scrimDarkMediumContrast,
|
||||
inverseSurface = inverseSurfaceDarkMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
|
||||
inversePrimary = inversePrimaryDarkMediumContrast,
|
||||
surfaceDim = surfaceDimDarkMediumContrast,
|
||||
surfaceBright = surfaceBrightDarkMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
|
||||
surfaceContainer = surfaceContainerDarkMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkHighContrast,
|
||||
onPrimary = onPrimaryDarkHighContrast,
|
||||
primaryContainer = primaryContainerDarkHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
|
||||
secondary = secondaryDarkHighContrast,
|
||||
onSecondary = onSecondaryDarkHighContrast,
|
||||
secondaryContainer = secondaryContainerDarkHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
|
||||
tertiary = tertiaryDarkHighContrast,
|
||||
onTertiary = onTertiaryDarkHighContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
|
||||
error = errorDarkHighContrast,
|
||||
onError = onErrorDarkHighContrast,
|
||||
errorContainer = errorContainerDarkHighContrast,
|
||||
onErrorContainer = onErrorContainerDarkHighContrast,
|
||||
background = backgroundDarkHighContrast,
|
||||
onBackground = onBackgroundDarkHighContrast,
|
||||
surface = surfaceDarkHighContrast,
|
||||
onSurface = onSurfaceDarkHighContrast,
|
||||
surfaceVariant = surfaceVariantDarkHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
|
||||
outline = outlineDarkHighContrast,
|
||||
outlineVariant = outlineVariantDarkHighContrast,
|
||||
scrim = scrimDarkHighContrast,
|
||||
inverseSurface = inverseSurfaceDarkHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
|
||||
inversePrimary = inversePrimaryDarkHighContrast,
|
||||
surfaceDim = surfaceDimDarkHighContrast,
|
||||
surfaceBright = surfaceBrightDarkHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
|
||||
surfaceContainer = surfaceContainerDarkHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
||||
)
|
||||
|
||||
val extendedLight = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningLight,
|
||||
onWarningLight,
|
||||
warningContainerLight,
|
||||
onWarningContainerLight,
|
||||
),
|
||||
)
|
||||
|
||||
val extendedDark = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningDark,
|
||||
onWarningDark,
|
||||
warningContainerDark,
|
||||
onWarningContainerDark,
|
||||
),
|
||||
)
|
||||
|
||||
val extendedLightMediumContrast = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningLightMediumContrast,
|
||||
onWarningLightMediumContrast,
|
||||
warningContainerLightMediumContrast,
|
||||
onWarningContainerLightMediumContrast,
|
||||
),
|
||||
)
|
||||
|
||||
val extendedLightHighContrast = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningLightHighContrast,
|
||||
onWarningLightHighContrast,
|
||||
warningContainerLightHighContrast,
|
||||
onWarningContainerLightHighContrast,
|
||||
),
|
||||
)
|
||||
|
||||
val extendedDarkMediumContrast = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningDarkMediumContrast,
|
||||
onWarningDarkMediumContrast,
|
||||
warningContainerDarkMediumContrast,
|
||||
onWarningContainerDarkMediumContrast,
|
||||
),
|
||||
)
|
||||
|
||||
val extendedDarkHighContrast = ExtendedColorScheme(
|
||||
warning = ColorFamily(
|
||||
warningDarkHighContrast,
|
||||
onWarningDarkHighContrast,
|
||||
warningContainerDarkHighContrast,
|
||||
onWarningContainerDarkHighContrast,
|
||||
),
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ColorFamily(
|
||||
val color: Color,
|
||||
val onColor: Color,
|
||||
val colorContainer: Color,
|
||||
val onColorContainer: Color
|
||||
)
|
||||
48
app/test/processing/app/LocaleKtTest.kt
Normal file
48
app/test/processing/app/LocaleKtTest.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
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 processing.app.ui.theme.LocalLocale
|
||||
import processing.app.ui.theme.LocaleProvider
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.test.Test
|
||||
|
||||
class LocaleKtTest {
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun testLocale() = runComposeUiTest {
|
||||
val tempPreferencesDir = createTempDirectory("preferences")
|
||||
|
||||
System.setProperty("processing.settings.folder", tempPreferencesDir.toFile().absolutePath)
|
||||
|
||||
setContent {
|
||||
LocaleProvider {
|
||||
val locale = LocalLocale.current
|
||||
Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText"))
|
||||
|
||||
Button(onClick = {
|
||||
locale.set(java.util.Locale("es"))
|
||||
}, modifier = Modifier.testTag("button")) {
|
||||
Text("Change")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if usage generates the language file if it doesn't exist
|
||||
val languageFile = tempPreferencesDir.resolve("language.txt").toFile()
|
||||
assert(languageFile.exists())
|
||||
|
||||
// Check if the text is localised
|
||||
onNodeWithTag("localisedText").assertTextEquals("New")
|
||||
|
||||
// Change the locale to Spanish
|
||||
onNodeWithTag("button").performClick()
|
||||
onNodeWithTag("localisedText").assertTextEquals("Nuevo")
|
||||
|
||||
// Check if the preference was saved to file
|
||||
assert(languageFile.readText().substring(0, 2) == "es")
|
||||
}
|
||||
}
|
||||
61
app/test/processing/app/PreferencesKtTest.kt
Normal file
61
app/test/processing/app/PreferencesKtTest.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
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 java.util.Properties
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.test.Test
|
||||
|
||||
class PreferencesKtTest{
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun testKeyReactivity() = runComposeUiTest {
|
||||
val directory = createTempDirectory("preferences")
|
||||
val tempPreferences = directory
|
||||
.resolve("preferences.txt")
|
||||
.createFile()
|
||||
.toFile()
|
||||
|
||||
// Set system properties for testing
|
||||
System.setProperty("processing.app.preferences.file", tempPreferences.absolutePath)
|
||||
System.setProperty("processing.app.preferences.debounce", "0")
|
||||
System.setProperty("processing.app.watchfile.forced", "true")
|
||||
|
||||
val newValue = (0..Int.MAX_VALUE).random().toString()
|
||||
val testKey = "test.preferences.reactivity"
|
||||
|
||||
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)
|
||||
|
||||
val preferences = Properties()
|
||||
preferences.load(tempPreferences.inputStream().reader(Charsets.UTF_8))
|
||||
|
||||
// Check if the preference was saved to file
|
||||
assert(preferences[testKey] == newValue)
|
||||
|
||||
|
||||
val nextValue = (0..Int.MAX_VALUE).random().toString()
|
||||
// Overwrite the file to see if the UI updates
|
||||
tempPreferences.writeText("$testKey=${nextValue}")
|
||||
|
||||
onNodeWithTag("text").assertTextEquals(nextValue)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
plugins {
|
||||
kotlin("jvm") version libs.versions.kotlin apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
alias(libs.plugins.jetbrainsCompose) apply false
|
||||
|
||||
BIN
build/shared/lib/fonts/SpaceGrotesk-Bold.ttf
Normal file
BIN
build/shared/lib/fonts/SpaceGrotesk-Bold.ttf
Normal file
Binary file not shown.
93
build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
Normal file
93
build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
Normal file
@@ -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.
|
||||
BIN
build/shared/lib/fonts/SpaceGrotesk-Light.ttf
Normal file
BIN
build/shared/lib/fonts/SpaceGrotesk-Light.ttf
Normal file
Binary file not shown.
BIN
build/shared/lib/fonts/SpaceGrotesk-Medium.ttf
Normal file
BIN
build/shared/lib/fonts/SpaceGrotesk-Medium.ttf
Normal file
Binary file not shown.
BIN
build/shared/lib/fonts/SpaceGrotesk-Regular.ttf
Normal file
BIN
build/shared/lib/fonts/SpaceGrotesk-Regular.ttf
Normal file
Binary file not shown.
BIN
build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf
Normal file
BIN
build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf
Normal file
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
[versions]
|
||||
kotlin = "2.0.20"
|
||||
compose-plugin = "1.7.1"
|
||||
kotlin = "2.2.20"
|
||||
compose-plugin = "1.9.1"
|
||||
jogl = "2.5.0"
|
||||
antlr = "4.13.2"
|
||||
jupiter = "5.12.0"
|
||||
markdown = "0.37.0"
|
||||
|
||||
[libraries]
|
||||
jogl = { module = "org.jogamp.jogl:jogl-all-main", version.ref = "jogl" }
|
||||
@@ -31,14 +32,14 @@ antlr4Runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" }
|
||||
composeGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" }
|
||||
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
kotlinComposePlugin = { module = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin", version.ref = "kotlin" }
|
||||
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" }
|
||||
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
|
||||
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdown" }
|
||||
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version.ref = "markdown" }
|
||||
clikt = { module = "com.github.ajalt.clikt:clikt", version = "5.0.2" }
|
||||
kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" }
|
||||
material3 = { module = "org.jetbrains.compose.material3:material3", version = "1.9.0" }
|
||||
|
||||
[plugins]
|
||||
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
download = { id = "de.undercouch.download", version = "5.6.0" }
|
||||
|
||||
@@ -277,13 +277,7 @@ public class JavaEditor extends Editor {
|
||||
|
||||
item = new JMenuItem(Language.text("menu.help.welcome"));
|
||||
item.addActionListener(e -> {
|
||||
try {
|
||||
new Welcome(base);
|
||||
} catch (IOException ioe) {
|
||||
Messages.showWarning("Unwelcome Error",
|
||||
"Please report this error to\n" +
|
||||
"https://github.com/processing/processing4/issues", ioe);
|
||||
}
|
||||
PDEWelcomeKt.showWelcomeScreen(base);
|
||||
});
|
||||
menu.add(item);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user