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:
Stef Tervelde
2025-12-09 16:14:01 +01:00
committed by GitHub
parent 467cee749b
commit 1237c55ece
39 changed files with 2720 additions and 819 deletions

View File

@@ -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 {

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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...

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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");

View File

@@ -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()
}
}

View File

@@ -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() }
}
}
}
}

View File

@@ -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
}

View File

@@ -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))
}
}
}
}

View File

@@ -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

View File

@@ -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);

View 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()
}
}
}
}

View File

@@ -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)

View File

@@ -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
)
}
}
}

View File

@@ -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()
}
}
}

View 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,
)

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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),
)

View 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!")
}
}
}
}

View 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)

View 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
)

View 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")
}
}

View 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)
}
}

View File

@@ -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

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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" }

View File

@@ -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);