├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ ├── module-info.java │ ├── sun │ │ └── net │ │ │ └── www │ │ │ └── protocol │ │ │ └── css │ │ │ └── Handler.kt │ └── tornadofx │ │ ├── Animation.kt │ │ ├── App.kt │ │ ├── Async.kt │ │ ├── AutoCompleteComboBoxSkin.kt │ │ ├── Binding.kt │ │ ├── CSS.kt │ │ ├── Charts.kt │ │ ├── ChildInterceptor.kt │ │ ├── Collections.kt │ │ ├── Commands.kt │ │ ├── Component.kt │ │ ├── Controls.kt │ │ ├── DataGrid.kt │ │ ├── Decoration.kt │ │ ├── Dialogs.kt │ │ ├── Drawer.kt │ │ ├── ErrorHandler.kt │ │ ├── EventBus.kt │ │ ├── EventLog.kt │ │ ├── FX.kt │ │ ├── Forms.kt │ │ ├── InternalWindow.kt │ │ ├── ItemControls.kt │ │ ├── Json.kt │ │ ├── Keyboard.kt │ │ ├── LayoutDebugger.kt │ │ ├── Layouts.kt │ │ ├── Lib.kt │ │ ├── ListMenu.kt │ │ ├── ListView.kt │ │ ├── Menu.kt │ │ ├── Messages.kt │ │ ├── Nodes.kt │ │ ├── Properties.kt │ │ ├── ReflectionUtils.java │ │ ├── Rest.kt │ │ ├── Shapes.kt │ │ ├── ShutdownExecutor.kt │ │ ├── Slideshow.kt │ │ ├── SmartResize.kt │ │ ├── SqueezeBox.kt │ │ ├── StackPane.kt │ │ ├── TabPane.kt │ │ ├── TableView.kt │ │ ├── TreeView.kt │ │ ├── Validation.kt │ │ ├── VectorMath.kt │ │ ├── ViewModel.kt │ │ ├── ViewModelFacades.kt │ │ ├── Wizard.kt │ │ ├── Workspace.kt │ │ ├── adapters │ │ ├── TornadoFXColumns.kt │ │ ├── TornadoFXResizeFeatures.kt │ │ └── TornadoFXTables.kt │ │ ├── osgi │ │ ├── ApplicationProvider.kt │ │ ├── ChildInterceptorProvider.kt │ │ ├── OSGIConsole.kt │ │ ├── StylesheetProvider.kt │ │ ├── ViewProvider.kt │ │ └── impl │ │ │ ├── Activator.kt │ │ │ ├── ApplicationListener.kt │ │ │ ├── CSSURLStreamHandlerService.kt │ │ │ ├── InterceptorListener.kt │ │ │ ├── OSGISupport.kt │ │ │ ├── StylesheetListener.kt │ │ │ └── ViewListener.kt │ │ └── skin │ │ └── tablerow │ │ ├── DirtyDecoratingTableRowSkin.java │ │ └── ExpandableTableRowSkin.java └── resources │ └── tornadofx │ ├── datagrid.css │ ├── form.css │ ├── i18n │ ├── ViewModel.properties │ ├── ViewModel_de_DE.properties │ ├── ViewModel_es.properties │ ├── ViewModel_nb_NO.properties │ ├── ViewModel_nl_NL.properties │ ├── ViewModel_ru_RU.properties │ ├── ViewModel_zh_CN.properties │ ├── Wizard.properties │ ├── Wizard_de_DE.properties │ ├── Wizard_es.properties │ ├── Wizard_nb_NO.properties │ ├── Wizard_nl_NL.properties │ ├── Wizard_ru_RU.properties │ └── Wizard_zh_CN.properties │ ├── listmenu.css │ ├── maskpane.css │ ├── rowexpanderpane.css │ └── workspace.css └── test ├── kotlin └── tornadofx │ ├── testapps │ ├── AccelTest.kt │ ├── AnimationHelperTest.kt │ ├── AsyncProgressApp.kt │ ├── AwaitUntilTest.kt │ ├── BorderPaneTest.kt │ ├── BrokenLineTest.kt │ ├── BuilderWindowTest.kt │ ├── ChartTestApp.kt │ ├── ComboBoxTest.kt │ ├── CommandTest.kt │ ├── DataGridTest.kt │ ├── DrawerTest.kt │ ├── EventBusTest.kt │ ├── ExpandableTableTest.kt │ ├── ExternalStylesheetTest.kt │ ├── FormTest.kt │ ├── GotoViewApp.kt │ ├── ImportStyles.kt │ ├── InternalWindowTest.kt │ ├── KeyboardTestView.kt │ ├── ListMenuTest.kt │ ├── MultipleLifecycleAsyncApp.kt │ ├── NestedTableColumns.kt │ ├── NewViewTransition.kt │ ├── PaddingPropertyTest.kt │ ├── PaintBackgroundTest.kt │ ├── ParamTest.kt │ ├── PojoTableColumns.kt │ ├── PojoTreeTableColumns.kt │ ├── ReloadStylesInModal.kt │ ├── RemoveTest.kt │ ├── ShortLongPressTest.kt │ ├── SlideshowTest.kt │ ├── SqueezeBoxTest.kt │ ├── StylesheetErrorTest.kt │ ├── SwitchViewApp.kt │ ├── TabPaneTest.kt │ ├── TableViewCellCacheTest.kt │ ├── TableViewCheckboxReflection.kt │ ├── TableViewDNDTest.kt │ ├── TableViewDirtyTest.kt │ ├── TableViewSelectionTest.kt │ ├── TableViewSortFilterTest.kt │ ├── TaskStatusTest.kt │ ├── TodoTestApp.kt │ ├── ToggleButtonTestApp.kt │ ├── TransitionsTest.kt │ ├── ViewDockingApp.kt │ └── WizardTestApp.kt │ └── tests │ ├── AsyncTest.kt │ ├── BindingTest.kt │ ├── BuildersTest.kt │ ├── CSSTest.kt │ ├── ChildInterceptorTest.kt │ ├── ClipboardTest.kt │ ├── CollectionsTest.kt │ ├── ComponentTest.kt │ ├── ControlsTest.kt │ ├── Customer.kt │ ├── EventBusTest.kt │ ├── FXMLTest.kt │ ├── FormTest.kt │ ├── InternalWindowClosingTest.kt │ ├── JavaPerson.java │ ├── JsonTest.kt │ ├── LayoutsTest.kt │ ├── ListTest.kt │ ├── ListViewFragmentTestView.kt │ ├── MultipleLifecycleAsyncAppTest.kt │ ├── NodeTest.kt │ ├── Person.kt │ ├── PersonPoko.kt │ ├── PreferencesTest.kt │ ├── PropertiesTest.kt │ ├── RestClientTest.kt │ ├── ScopeTest.kt │ ├── SimpleListViewTest.kt │ ├── StylesheetErrorTest.kt │ ├── StylesheetTest.kt │ ├── TableViewTest.kt │ ├── ValidationTest.kt │ ├── VectorMathTest.kt │ ├── ViewDockingTests.kt │ └── ViewModelTest.kt └── resources ├── META-INF └── services │ └── tornadofx.ChildInterceptor ├── teststyles.css └── tornadofx └── tests ├── ComposedForm.fxml ├── ComposedFormList.fxml ├── ComposedFormSearch.fxml ├── FXMLTest.fxml ├── SimpleForm.fxml ├── TornadoKeyboard.json └── person.png /.gitignore: -------------------------------------------------------------------------------- 1 | # all target dirs 2 | target/ 3 | build/ 4 | out/ 5 | bin/ 6 | 7 | # IntelliJ. 8 | .idea/ 9 | *.iml 10 | *.ipr 11 | *.iws 12 | 13 | # Eclipse 14 | .settings/ 15 | .project 16 | .classpath 17 | 18 | # gradle 19 | .gradle/ 20 | !gradle-wrapper.jar 21 | !gradle-wrapper.properties 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: true 3 | 4 | os: linux 5 | dist: focal 6 | jdk: openjdk11 7 | 8 | services: 9 | - xvfb 10 | 11 | before_install: 12 | - chmod +x gradlew 13 | 14 | stages: 15 | - name: test 16 | - name: snapshot 17 | if: branch = master AND tag IS blank 18 | - name: release 19 | if: branch IN (master, release) AND tag =~ /^[Vv]{0,1}\d+\.\d+\.\d+.*/ 20 | 21 | jobs: 22 | include: 23 | - stage: test 24 | name: "Headed Tests" 25 | env: _JAVA_OPTIONS="-Dtestfx.robot=glass" 26 | script: ./gradlew check 27 | - # headless 28 | name: "Headless Tests" 29 | env: _JAVA_OPTIONS="-Djava.awt.headless=true -Dtestfx.robot=glass -Dtestfx.headless=true -Dglass.platform=Monocle -Dmonocle.platform=Headless -Dprism.order=sw" 30 | script: ./gradlew check 31 | - stage: snapshot 32 | script: echo "snapshot build" 33 | - stage: release 34 | script: echo "release build" 35 | 36 | before_cache: 37 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 38 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 39 | - rm -f $HOME/.gradle/caches/*/fileHashes/fileHashes.bin 40 | - rm -f $HOME/.gradle/caches/*/fileHashes/fileHashes.lock 41 | 42 | cache: 43 | directories: 44 | - $HOME/.gradle/caches/ 45 | - $HOME/.gradle/wrapper/ 46 | - $HOME/.m2 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TornadoFX v2 Changelog 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TornadoFX v2 (Gradle) 2 | 3 | A JavaFX framework for Kotlin (Java 11+) 4 | 5 | This (experimental) branch is gradle based. 6 | Current state: TODO 7 | 8 | * Kotlin & Java target is Java 11.0.2 9 | * JavaFX version is 15.0.1 10 | * JUnit5 with additional vintage engine for unit testing 11 | * Tests running on classpath ignoring javas module system 12 | * no support for OSGI bundle 13 | 14 | ## Features 15 | 16 | * Supports both MVC, MVP and their derivatives 17 | * Dependency injection 18 | * Type safe GUI builders 19 | * Type safe CSS builders 20 | * First class FXML support 21 | * Async task execution 22 | * EventBus with thread targeting 23 | * Hot reload of Views and Stylesheets 24 | * Zero config, no XML, no annotations 25 | 26 | ## Requirements 27 | 28 | * JDK 11+ 29 | * [Kotlin 1.3+](https://kotlinlang.org/) 30 | * [JavaFX 13+](https://openjfx.io/) 31 | 32 | ## Installation 33 | 34 | In repositories block add 35 | 36 | ``` 37 | maven { 38 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 39 | } 40 | ``` 41 | 42 | In dependencies block add 43 | ``` 44 | implementation("no.tornado:tornadofx:2.0.0-SNAPSHOT") 45 | ``` 46 | 47 | ## Feature Requests and Bugs 48 | 49 | Please use the GitHub [issues](https://github.com/edvin/tornadofx2/issues) for all feature requests and bugs. 50 | 51 | ## Maintainers 52 | 53 | * [Edvin Syse](https://github.com/edvin) 54 | * [GoToTags](https://gototags.com/) / [Craig Tadlock](https://www.linkedin.com/in/ctadlock/) 55 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.daemon=false 3 | org.gradle.jvmargs=-Xmx1024m 4 | #deps 5 | kotlin_version=1.3.61 6 | tornado_version=2.0.0-SNAPSHOT 7 | dokka_version=1.4.20 8 | json_version=1.1.2 9 | httpclient_version=4.5.3 10 | felix_framework_version=6.0.1 11 | junit4_version=4.13 12 | junit5_version=5.7.0 13 | testfx_version=4.0.16-alpha 14 | hamcrest_version=2.2 15 | fontawesomefx_version=4.7.0-9.1.2 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edvin/tornadofx2/17c1da828586b0cba09a6aa3a35a8c13a4a9d956/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'tornadofx2' 2 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | import tornadofx.ChildInterceptor; 2 | import tornadofx.Stylesheet; 3 | 4 | module tornadofx { 5 | requires transitive javafx.controls; 6 | requires javafx.fxml; 7 | requires javafx.swing; 8 | requires javafx.web; 9 | requires javafx.media; 10 | 11 | requires kotlin.stdlib; 12 | requires kotlin.reflect; 13 | 14 | requires static httpcore; 15 | requires static httpclient; 16 | requires static org.apache.felix.framework; 17 | 18 | requires transitive java.json; 19 | requires transitive java.prefs; 20 | requires transitive java.logging; 21 | 22 | opens tornadofx to javafx.fxml; 23 | 24 | exports tornadofx; 25 | 26 | uses ChildInterceptor; 27 | uses Stylesheet; 28 | } -------------------------------------------------------------------------------- /src/main/java/sun/net/www/protocol/css/Handler.kt: -------------------------------------------------------------------------------- 1 | package sun.net.www.protocol.css 2 | 3 | import tornadofx.FX 4 | import tornadofx.Stylesheet 5 | import java.io.InputStream 6 | import java.net.URL 7 | import java.net.URLConnection 8 | import java.net.URLStreamHandler 9 | import java.net.URLStreamHandlerFactory 10 | import java.nio.charset.StandardCharsets 11 | import java.util.* 12 | 13 | open class Handler : URLStreamHandler() { 14 | override fun openConnection(url: URL): URLConnection = CSSURLConnection(url) 15 | 16 | class CSSURLConnection(url: URL) : URLConnection(url) { 17 | override fun connect() { } 18 | override fun getInputStream(): InputStream { 19 | if (url.port == 64) return Base64.getDecoder().decode(url.host).inputStream() 20 | val stylesheet = Class.forName(url.host).newInstance() as Stylesheet 21 | val rendered = stylesheet.render() 22 | if (FX.dumpStylesheets) println(rendered) 23 | return rendered.byteInputStream(StandardCharsets.UTF_8) 24 | } 25 | } 26 | 27 | class HandlerFactory : URLStreamHandlerFactory { 28 | override fun createURLStreamHandler(protocol: String) = 29 | if ("css" == protocol) Handler() else null 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/ChildInterceptor.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | import javafx.event.EventTarget 4 | import javafx.scene.Node 5 | /** 6 | * An interceptor that can veto or provide another mechanism for adding children to their parent. 7 | * 8 | * All interceptors called for all builders right before the default mechanism adds the newly created 9 | * node to the parent container. Returning false means that the default will be used. Returning true means 10 | * that you added the child yourself and that the default mechanism should do nothing. 11 | * 12 | * This is useful for layout containers that need special handling when adding child nodes. 13 | * 14 | * Example: MigPane needs to add a default ComponentConstraint object to be able to manipulate the 15 | * constraint after the child is added. 16 | * ``` 17 | * class MigPaneChildInterceptor: ChildInterceptor{ 18 | * override fun invoke(parent: EventTarget, node: Node, index: Int?): Boolean{ 19 | * return when(parent){ 20 | * is MigPane -> {parent.add(node, CC()); true} 21 | * else -> false 22 | * } 23 | * } 24 | * } 25 | * ``` 26 | * @sample tornadofx.tests.FirstInterceptor 27 | * @sample tornadofx.tests.SecondInterceptor 28 | * 29 | */ 30 | interface ChildInterceptor { 31 | operator fun invoke(parent: EventTarget, node: Node, index: Int?):Boolean 32 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/Decoration.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | import javafx.beans.value.ChangeListener 4 | import javafx.beans.value.ObservableValue 5 | import javafx.scene.Node 6 | import javafx.scene.Parent 7 | import javafx.scene.control.Control 8 | import javafx.scene.control.Tooltip 9 | import javafx.scene.paint.Color 10 | import javafx.scene.shape.Polygon 11 | 12 | interface Decorator { 13 | fun decorate(node: Node) 14 | fun undecorate(node: Node) 15 | } 16 | 17 | fun Node.addDecorator(decorator: Decorator) { 18 | decorators += decorator 19 | decorator.decorate(this) 20 | } 21 | 22 | fun Node.removeDecorator(decorator: Decorator) { 23 | decorators -= decorator 24 | decorator.undecorate(this) 25 | } 26 | 27 | @Suppress("UNCHECKED_CAST") 28 | val Node.decorators: MutableList get() = properties.getOrPut("tornadofx.decorators", { mutableListOf() }) as MutableList 29 | 30 | class SimpleMessageDecorator(val message: String?, severity: ValidationSeverity) : Decorator { 31 | val color: Color = when (severity) { 32 | ValidationSeverity.Error -> Color.RED 33 | ValidationSeverity.Warning -> Color.ORANGE 34 | ValidationSeverity.Success -> Color.GREEN 35 | else -> Color.BLUE 36 | } 37 | var tag: Polygon? = null 38 | var tooltip: Tooltip? = null 39 | var attachedToNode: Node? = null 40 | 41 | var focusListener = ChangeListener { _, _, newValue -> 42 | if (newValue == true) showTooltip(attachedToNode!!) else tooltip?.hide() 43 | } 44 | 45 | override fun decorate(node: Node) { 46 | attachedToNode = node 47 | if (node is Parent) { 48 | tag = node.polygon(0.0, 0.0, 0.0, 10.0, 10.0, 0.0) { 49 | isManaged = false 50 | fill = color 51 | } 52 | } 53 | 54 | if (message?.isNotBlank() ?: false) { 55 | tooltip = Tooltip(message) 56 | if (node is Control) node.tooltip = tooltip else Tooltip.install(node, tooltip) 57 | if (node.isFocused) showTooltip(node) 58 | node.focusedProperty().addListener(focusListener) 59 | } 60 | } 61 | 62 | private fun showTooltip(node: Node) { 63 | tooltip?.apply { 64 | if (isShowing) return 65 | val b = node.localToScreen(node.boundsInLocal) 66 | if (b != null) show(node, b.minX + 5, b.maxY) 67 | } 68 | } 69 | 70 | override fun undecorate(node: Node) { 71 | tag?.removeFromParent() 72 | tooltip?.apply { 73 | hide() 74 | Tooltip.uninstall(node, this) 75 | } 76 | node.focusedProperty().removeListener(focusListener) 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/EventLog.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | class MessageEvent(val message: String, val severity: ValidationSeverity) : FXEvent() { 4 | enum class Severity { Info, Warning, Error } 5 | } 6 | 7 | class EventLogView -------------------------------------------------------------------------------- /src/main/java/tornadofx/Messages.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | import java.io.InputStreamReader 6 | import java.nio.charset.StandardCharsets 7 | import java.security.AccessController 8 | import java.security.PrivilegedActionException 9 | import java.security.PrivilegedExceptionAction 10 | import java.text.MessageFormat 11 | import java.util.* 12 | 13 | private fun doPrivileged(privilegedAction: ()->RETURN) : RETURN = try { 14 | AccessController.doPrivileged( 15 | PrivilegedExceptionAction { privilegedAction() } 16 | ) 17 | } catch (e: PrivilegedActionException) { 18 | @Suppress("UNCHECKED_CAST") 19 | throw e.exception as EXCEPTION 20 | } 21 | 22 | 23 | object FXResourceBundleControl : ResourceBundle.Control() { 24 | 25 | override fun newBundle(baseName: String, locale: Locale, format: String, loader: ClassLoader, reload: Boolean): ResourceBundle? { 26 | val bundleName = toBundleName(baseName, locale) 27 | return when (format) { 28 | 29 | "java.class" -> try { 30 | @Suppress("UNCHECKED_CAST") 31 | val bundleClass = loader.loadClass(bundleName) as Class 32 | 33 | // If the class isn't a ResourceBundle subclass, throw a ClassCastException. 34 | if (ResourceBundle::class.java.isAssignableFrom(bundleClass)) bundleClass.newInstance() 35 | else throw ClassCastException(bundleClass.name + " cannot be cast to ResourceBundle") 36 | 37 | } catch (e: ClassNotFoundException) { null } 38 | "java.properties" -> { 39 | val resourceName: String = toResourceName(bundleName, "properties") 40 | doPrivileged { 41 | if (!reload) loader.getResourceAsStream(resourceName) 42 | else loader.getResource(resourceName)?.openConnection()?.apply { 43 | useCaches = false // Disable caches to get fresh data for reloading. 44 | } ?.inputStream 45 | }?.use { FXPropertyResourceBundle(InputStreamReader(it, StandardCharsets.UTF_8)) } 46 | } 47 | else -> throw IllegalArgumentException("unknown format: $format") 48 | }!! 49 | } 50 | } 51 | 52 | /** 53 | * Convenience function to support lookup via messages["key"] 54 | */ 55 | operator fun ResourceBundle.get(key: String) = getString(key) 56 | 57 | /** 58 | * Convenience function to retrieve a translation and format it with values. 59 | */ 60 | fun ResourceBundle.format(key: String, vararg fields: Any) = 61 | MessageFormat(this[key], locale).format(fields) 62 | 63 | class FXPropertyResourceBundle(input: InputStreamReader): PropertyResourceBundle(input) { 64 | fun inheritFromGlobal() { 65 | parent = FX.messages.takeUnless(::equals) 66 | } 67 | 68 | /** 69 | * Lookup resource in this bundle. If no value, lookup in parent bundle if defined. 70 | * If we still have no value, return "[key]" instead of null. 71 | */ 72 | override fun handleGetObject(key: String?) = super.handleGetObject(key) 73 | ?: parent?.getObject(key) 74 | ?: "[$key]" 75 | 76 | /** 77 | * Always return true, since we want to supply a default text message instead of throwing exception 78 | */ 79 | override fun containsKey(key: String) = true 80 | } 81 | 82 | internal object EmptyResourceBundle : ResourceBundle() { 83 | override fun getKeys(): Enumeration = Collections.emptyEnumeration() 84 | override fun handleGetObject(key: String) = "[$key]" 85 | override fun containsKey(p0: String) = true 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package tornadofx; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.Arrays; 6 | 7 | public class ReflectionUtils { 8 | 9 | public static void callMethod(Object object, String methodName, Object... params) { 10 | Class clazz = object.getClass(); 11 | callMethod(object, clazz, methodName, params); 12 | } 13 | 14 | public static void callMethod(Object object, Class clazz, String methodName, Object... params) { 15 | try { 16 | Method method = clazz.getDeclaredMethod(methodName, Arrays.stream(params).map(Object::getClass).toArray(Class[]::new)); 17 | method.setAccessible(true); 18 | method.invoke(object, params); 19 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 20 | throw new RuntimeException("Cannot call method " + methodName + " on " + object.getClass()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/Shapes.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | import javafx.scene.Parent 4 | import javafx.scene.shape.* 5 | 6 | fun Parent.arc(centerX: Number = 0.0, centerY: Number = 0.0, radiusX: Number = 0.0, radiusY: Number = 0.0, startAngle: Number = 0.0, length: Number = 0.0, op: Arc.() -> Unit = {}) = 7 | Arc(centerX.toDouble(), centerY.toDouble(), radiusX.toDouble(), radiusY.toDouble(), startAngle.toDouble(), length.toDouble()).attachTo(this, op) 8 | 9 | fun Parent.circle(centerX: Number = 0.0, centerY: Number = 0.0, radius: Number = 0.0, op: Circle.() -> Unit = {}) = 10 | Circle(centerX.toDouble(), centerY.toDouble(), radius.toDouble()).attachTo(this, op) 11 | 12 | fun Parent.cubiccurve(startX: Number = 0.0, startY: Number = 0.0, controlX1: Number = 0.0, controlY1: Number = 0.0, controlX2: Number = 0.0, controlY2: Number = 0.0, endX: Number = 0.0, endY: Number = 0.0, op: CubicCurve.() -> Unit = {}) = 13 | CubicCurve(startX.toDouble(), startY.toDouble(), controlX1.toDouble(), controlY1.toDouble(), controlX2.toDouble(), controlY2.toDouble(), endX.toDouble(), endY.toDouble()).attachTo(this, op) 14 | 15 | fun Parent.ellipse(centerX: Number = 0.0, centerY: Number = 0.0, radiusX: Number = 0.0, radiusY: Number = 0.0, op: Ellipse.() -> Unit = {}) = 16 | Ellipse(centerX.toDouble(), centerY.toDouble(), radiusX.toDouble(), radiusY.toDouble()).attachTo(this, op) 17 | 18 | fun Parent.line(startX: Number = 0.0, startY: Number = 0.0, endX: Number = 0.0, endY: Number = 0.0, op: Line.() -> Unit = {}) = 19 | Line(startX.toDouble(), startY.toDouble(), endX.toDouble(), endY.toDouble()).attachTo(this, op) 20 | 21 | fun Parent.path(vararg elements: PathElement, op: Path.() -> Unit = {}) = Path(*elements).attachTo(this, op) 22 | 23 | fun Path.moveTo(x: Number = 0.0, y: Number = 0.0) = apply { 24 | elements.add(MoveTo(x.toDouble(), y.toDouble())) 25 | } 26 | 27 | fun Path.hlineTo(x: Number) = apply { elements.add(HLineTo(x.toDouble())) } 28 | 29 | fun Path.vlineTo(y: Number) = apply { elements.add(VLineTo(y.toDouble())) } 30 | 31 | fun Path.quadcurveTo(controlX: Number = 0.0, controlY: Number = 0.0, x: Number = 0.0, y: Number = 0.0, op: QuadCurveTo.() -> Unit = {}) = apply { 32 | elements.add(QuadCurveTo(controlX.toDouble(), controlY.toDouble(), x.toDouble(), y.toDouble()).also(op)) 33 | } 34 | 35 | fun Path.cubiccurveTo(controlX1: Number = 0.0, controlY1: Number = 0.0, controlX2: Number = 0.0, controlY2: Number = 0.0, x: Number = 0.0, y: Number = 0.0, op: CubicCurveTo.() -> Unit = {}) = apply { 36 | elements.add(CubicCurveTo(controlX1.toDouble(), controlY1.toDouble(), controlX2.toDouble(), controlY2.toDouble(), x.toDouble(), y.toDouble()).also(op)) 37 | } 38 | 39 | fun Path.lineTo(x: Number = 0.0, y: Number = 0.0) = apply { 40 | elements.add(LineTo(x.toDouble(), y.toDouble())) 41 | } 42 | 43 | fun Path.arcTo( 44 | radiusX: Number = 0.0, radiusY: Number = 0.0, 45 | xAxisRotation: Number = 0.0, x: Number = 0.0, 46 | y: Number = 0.0, largeArcFlag: Boolean = false, 47 | sweepFlag: Boolean = false, op: ArcTo.() -> Unit = {}) = apply{ 48 | elements.add(ArcTo(radiusX.toDouble(), radiusY.toDouble(), xAxisRotation.toDouble(), x.toDouble(), y.toDouble(), largeArcFlag, sweepFlag).also(op)) 49 | } 50 | 51 | fun Path.closepath() = apply { elements.add(ClosePath()) } 52 | 53 | fun Parent.polygon(vararg points: Number, op: Polygon.() -> Unit = {}) = 54 | Polygon(*points.map(Number::toDouble).toDoubleArray()).attachTo(this, op) 55 | 56 | fun Parent.polyline(vararg points: Number, op: Polyline.() -> Unit = {}) = 57 | Polyline(*points.map(Number::toDouble).toDoubleArray()).attachTo(this, op) 58 | 59 | fun Parent.quadcurve(startX: Number = 0.0, startY: Number = 0.0, controlX: Number = 0.0, controlY: Number = 0.0, endX: Number = 0.0, endY: Number = 0.0, op: QuadCurve.() -> Unit = {}) = 60 | QuadCurve(startX.toDouble(), startY.toDouble(), controlX.toDouble(), controlY.toDouble(), endX.toDouble(), endY.toDouble()).attachTo(this, op) 61 | 62 | fun Parent.rectangle(x: Number = 0.0, y: Number = 0.0, width: Number = 0.0, height: Number = 0.0, op: Rectangle.() -> Unit = {}) = 63 | Rectangle(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()).attachTo(this, op) 64 | 65 | fun Parent.svgpath(content: String? = null, fillRule: FillRule? = null, op: SVGPath.() -> Unit = {})= SVGPath().attachTo(this, op){ 66 | if (content != null) it.content = content 67 | if (fillRule != null) it.fillRule = fillRule 68 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/ShutdownExecutor.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | /** 4 | * 5 | * @author Anindya Chatterjee 6 | */ 7 | internal object ShutdownExecutor { 8 | // a task executor to run some routines before 9 | // graceful shutdown of JVM 10 | 11 | private var taskList = ArrayList<() -> Unit>() 12 | 13 | init { 14 | Runtime.getRuntime().addShutdownHook(Thread { 15 | // create the shutdown hook 16 | runBeforeShutdown() 17 | }) 18 | } 19 | 20 | fun registerTask(task: () -> Unit) { 21 | // add the task to a list 22 | taskList.add(task) 23 | } 24 | 25 | private fun runBeforeShutdown() { 26 | // run the task from list before shutdown 27 | taskList.forEach { it() } 28 | } 29 | } 30 | 31 | fun beforeShutdown(task: () -> Unit) { 32 | ShutdownExecutor.registerTask(task) 33 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/StackPane.kt: -------------------------------------------------------------------------------- 1 | package tornadofx 2 | 3 | import javafx.beans.binding.BooleanExpression 4 | import javafx.beans.property.ReadOnlyObjectProperty 5 | import javafx.beans.property.SimpleBooleanProperty 6 | import javafx.beans.property.SimpleObjectProperty 7 | import javafx.beans.value.ChangeListener 8 | import javafx.scene.Node 9 | import javafx.scene.layout.StackPane 10 | 11 | /** 12 | * This property always contains the topmost Node in the children 13 | * list of the StackPane 14 | */ 15 | val StackPane.contentProperty: ReadOnlyObjectProperty 16 | get() { 17 | @Suppress("UNCHECKED_CAST") 18 | return properties.getOrPut("tornadofx.contentProperty") { 19 | val p = SimpleObjectProperty(children.lastOrNull()) 20 | children.onChange { 21 | p.value = children.lastOrNull() 22 | } 23 | p 24 | } as ReadOnlyObjectProperty 25 | } 26 | 27 | val StackPane.content: Node? get() = contentProperty.value 28 | 29 | val StackPane.savable: BooleanExpression 30 | get() { 31 | val savable = SimpleBooleanProperty(true) 32 | 33 | fun updateState() { 34 | savable.cleanBind(contentUiComponent()?.savable 35 | ?: SimpleBooleanProperty(Workspace.defaultSavable)) 36 | } 37 | 38 | val contentChangeListener = ChangeListener { _, _, _ -> updateState() } 39 | 40 | updateState() 41 | 42 | contentProperty.addListener(contentChangeListener) 43 | 44 | return savable 45 | } 46 | 47 | val StackPane.creatable: BooleanExpression 48 | get() { 49 | val creatable = SimpleBooleanProperty(true) 50 | 51 | fun updateState() { 52 | creatable.cleanBind(contentUiComponent()?.creatable 53 | ?: SimpleBooleanProperty(Workspace.defaultCreatable)) 54 | } 55 | 56 | val contentChangeListener = ChangeListener { _, _, _ -> updateState() } 57 | 58 | updateState() 59 | 60 | contentProperty.addListener(contentChangeListener) 61 | 62 | return savable 63 | } 64 | 65 | val StackPane.deletable: BooleanExpression 66 | get() { 67 | val deletable = SimpleBooleanProperty(true) 68 | 69 | fun updateState() { 70 | deletable.cleanBind(contentUiComponent()?.deletable 71 | ?: SimpleBooleanProperty(Workspace.defaultDeletable)) 72 | } 73 | 74 | val contentChangeListener = ChangeListener { _, _, _ -> updateState() } 75 | 76 | updateState() 77 | 78 | contentProperty.addListener(contentChangeListener) 79 | 80 | return deletable 81 | } 82 | 83 | 84 | val StackPane.refreshable: BooleanExpression 85 | get() { 86 | val refreshable = SimpleBooleanProperty(true) 87 | 88 | fun updateState() { 89 | refreshable.cleanBind(contentUiComponent()?.refreshable 90 | ?: SimpleBooleanProperty(Workspace.defaultRefreshable)) 91 | } 92 | 93 | val contentChangeListener = ChangeListener { _, _, _ -> updateState() } 94 | 95 | updateState() 96 | 97 | contentProperty.addListener(contentChangeListener) 98 | 99 | return refreshable 100 | } 101 | 102 | inline fun StackPane.contentUiComponent(): T? = content?.uiComponent() 103 | fun StackPane.onDelete() = contentUiComponent()?.onDelete() 104 | fun StackPane.onSave() = contentUiComponent()?.onSave() 105 | fun StackPane.onCreate() = contentUiComponent()?.onCreate() 106 | fun StackPane.onRefresh() = contentUiComponent()?.onRefresh() 107 | fun StackPane.onNavigateBack() = contentUiComponent()?.onNavigateBack() ?: true 108 | fun StackPane.onNavigateForward() = contentUiComponent()?.onNavigateForward() ?: true 109 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/adapters/TornadoFXColumns.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.adapters 2 | 3 | import javafx.beans.property.DoubleProperty 4 | import javafx.scene.control.TableColumn 5 | import javafx.scene.control.TreeTableColumn 6 | 7 | fun TreeTableColumn<*,*>.toTornadoFXColumn() = TornadoFXTreeTableColumn(this) 8 | fun TableColumn<*,*>.toTornadoFXColumn() = TornadoFxNormalTableColumn(this) 9 | 10 | interface TornadoFXColumn { 11 | val column: COLUMN 12 | val properties: Properties 13 | var prefWidth: Double 14 | var maxWidth: Double 15 | var minWidth: Double 16 | val width: Double 17 | val isVisible: Boolean 18 | val minWidthProperty: DoubleProperty 19 | val maxWidthProperty: DoubleProperty 20 | fun isLegalWidth(width: Double) = width in minWidthProperty.get() .. maxWidthProperty.get() 21 | } 22 | 23 | class TornadoFXTreeTableColumn(override val column: TreeTableColumn<*, *>) : TornadoFXColumn> { 24 | override val minWidthProperty get() = column.minWidthProperty() 25 | override val maxWidthProperty get() = column.maxWidthProperty() 26 | override var minWidth: Double 27 | get() = column.minWidth 28 | set(value) { 29 | column.minWidth = value 30 | } 31 | override val properties = column.properties 32 | override var prefWidth: Double 33 | get() = column.prefWidth 34 | set(value) { 35 | column.prefWidth = value 36 | } 37 | override var maxWidth: Double 38 | get() = column.maxWidth 39 | set(value) { 40 | column.maxWidth = value 41 | } 42 | override val width: Double 43 | get() = column.width 44 | 45 | override val isVisible: Boolean 46 | get() = column.isVisible 47 | 48 | override fun equals(other: Any?): Boolean { 49 | if (this === other) return true 50 | if (javaClass != other?.javaClass) return false 51 | 52 | other as TornadoFXTreeTableColumn 53 | 54 | if (column != other.column) return false 55 | 56 | return true 57 | } 58 | 59 | override fun hashCode(): Int { 60 | return column.hashCode() 61 | } 62 | 63 | } 64 | 65 | class TornadoFxNormalTableColumn(override val column: TableColumn<*, *>) : TornadoFXColumn> { 66 | override var minWidth: Double 67 | get() = column.minWidth 68 | set(value) { 69 | column.minWidth = value 70 | } 71 | override var maxWidth: Double 72 | get() = column.maxWidth 73 | set(value) { 74 | column.maxWidth = value 75 | } 76 | override var prefWidth: Double 77 | get() = column.prefWidth 78 | set(value) { 79 | column.prefWidth = value 80 | } 81 | override val properties = column.properties 82 | override val width: Double 83 | get() = column.width 84 | override val minWidthProperty get() = column.minWidthProperty() 85 | override val maxWidthProperty get() = column.maxWidthProperty() 86 | 87 | override val isVisible: Boolean 88 | get() = column.isVisible 89 | 90 | override fun equals(other: Any?): Boolean { 91 | if (this === other) return true 92 | if (javaClass != other?.javaClass) return false 93 | 94 | other as TornadoFxNormalTableColumn 95 | 96 | if (column != other.column) return false 97 | 98 | return true 99 | } 100 | 101 | override fun hashCode(): Int { 102 | return column.hashCode() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/adapters/TornadoFXResizeFeatures.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.adapters 2 | 3 | import javafx.collections.ObservableMap 4 | import javafx.scene.control.TableColumn 5 | import javafx.scene.control.TableView 6 | import javafx.scene.control.TreeTableColumn 7 | import javafx.scene.control.TreeTableView 8 | import javafx.util.Callback 9 | 10 | typealias Properties = ObservableMap 11 | typealias TableViewResizeCallback = Callback, Boolean> 12 | typealias TreeTableViewResizeCallback = Callback, Boolean> 13 | 14 | fun TreeTableView.ResizeFeatures<*>.toTornadoFXResizeFeatures() = TornadoFXTreeTableResizeFeatures(this) 15 | fun TableView.ResizeFeatures<*>.toTornadoFXFeatures() = TornadoFxTableResizeFeatures(this) 16 | 17 | interface TornadoFXResizeFeatures { 18 | val table: TornadoFXTable 19 | val delta: Double 20 | val column: TornadoFXColumn? 21 | } 22 | 23 | class TornadoFXTreeTableResizeFeatures(val param: TreeTableView.ResizeFeatures) : TornadoFXResizeFeatures, TreeTableView<*>> { 24 | override val column = param.column?.toTornadoFXColumn() 25 | override val table = param.table.toTornadoFXTable() 26 | override val delta: Double get() = param.delta 27 | } 28 | 29 | class TornadoFxTableResizeFeatures(val param: TableView.ResizeFeatures) : TornadoFXResizeFeatures, TableView<*>> { 30 | override val table: TornadoFXTable, TableView<*>> = TornadoFXNormalTable(param.table) 31 | override val delta: Double = param.delta 32 | override val column: TornadoFXColumn>? = param.column?.let { TornadoFxNormalTableColumn(it) } 33 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/adapters/TornadoFXTables.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.adapters 2 | 3 | import javafx.beans.property.ObjectProperty 4 | import javafx.scene.control.* 5 | import javafx.util.Callback 6 | 7 | fun TreeTableView<*>.toTornadoFXTable() = TornadoFXTreeTable(this) 8 | fun TableView<*>.toTornadoFXTable() : TornadoFXTable, TableView<*>> = TornadoFXNormalTable(this) 9 | 10 | interface TornadoFXTable { 11 | val table: TABLE 12 | val contentWidth: Double 13 | val properties: Properties 14 | val contentColumns: List> 15 | 16 | var skin: Skin<*>? 17 | 18 | val skinProperty: ObjectProperty> 19 | } 20 | 21 | class TornadoFXTreeTable(override val table: TreeTableView<*>) : TornadoFXTable, TreeTableView<*>> { 22 | override val skinProperty = table.skinProperty() 23 | 24 | override var skin 25 | get() = table.skin 26 | set(value) { 27 | table.skin = value 28 | } 29 | 30 | override val contentColumns get() = table.columns.flatMap { 31 | if (it.columns.isEmpty()) listOf(it) else it.columns 32 | }.map { it.toTornadoFXColumn() } 33 | 34 | override val properties = table.properties 35 | 36 | private val contentWidthField by lazy { 37 | TreeTableView::class.java.getDeclaredField("contentWidth").also { 38 | it.isAccessible = true 39 | } 40 | } 41 | 42 | override val contentWidth get() = contentWidthField.get(table) as Double 43 | 44 | var columnResizePolicy: Callback, Boolean> 45 | get() = table.columnResizePolicy 46 | set(value) { 47 | table.columnResizePolicy = value 48 | } 49 | } 50 | 51 | class TornadoFXNormalTable(override val table: TableView<*>) : TornadoFXTable, TableView<*>> { 52 | override val skinProperty: ObjectProperty> get() = table.skinProperty() 53 | 54 | override val contentColumns get() = table.columns.flatMap { 55 | if (it.columns.isEmpty()) listOf(it) else it.columns 56 | }.map { it.toTornadoFXColumn() } 57 | 58 | override var skin 59 | get() = table.skin 60 | set(value) { 61 | table.skin = value 62 | } 63 | 64 | private val contentWidthField by lazy { 65 | TableView::class.java.getDeclaredField("contentWidth").also { 66 | it.isAccessible = true 67 | } 68 | } 69 | 70 | override val contentWidth get() = contentWidthField.get(table) as Double 71 | override val properties = table.properties 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/ApplicationProvider.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi 2 | 3 | import org.osgi.framework.BundleContext 4 | import tornadofx.App 5 | import java.util.* 6 | import kotlin.reflect.KClass 7 | 8 | interface ApplicationProvider { 9 | val application: KClass 10 | } 11 | 12 | inline fun BundleContext.registerApplication() = registerApplication(T::class) 13 | fun BundleContext.registerApplication(application: KClass) { 14 | val provider = object : ApplicationProvider { 15 | override val application = application 16 | } 17 | registerService(ApplicationProvider::class.java, provider, Hashtable()) 18 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/ChildInterceptorProvider.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi 2 | 3 | import org.osgi.framework.BundleContext 4 | import tornadofx.* 5 | import java.util.* 6 | import kotlin.reflect.KClass 7 | import kotlin.reflect.full.createInstance 8 | 9 | interface ChildInterceptorProvider { 10 | val interceptor: ChildInterceptor 11 | } 12 | 13 | inline fun BundleContext.registerChildInterceptor() = registerChildInterceptor(T::class) 14 | fun BundleContext.registerChildInterceptor(interceptorKlass: KClass) { 15 | val provider = object : ChildInterceptorProvider { 16 | override val interceptor = interceptorKlass.createInstance() 17 | } 18 | registerService(ChildInterceptorProvider::class.java, provider, Hashtable()) 19 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/OSGIConsole.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi 2 | 3 | import javafx.scene.control.TableView 4 | import javafx.scene.control.TextInputDialog 5 | import javafx.scene.input.TransferMode 6 | import javafx.stage.FileChooser 7 | import org.osgi.framework.Bundle 8 | import org.osgi.framework.Bundle.ACTIVE 9 | import org.osgi.framework.startlevel.BundleStartLevel 10 | import tornadofx.* 11 | import tornadofx.osgi.impl.fxBundleContext 12 | import java.nio.file.Files 13 | 14 | class OSGIConsole : View() { 15 | override val root = borderpane { 16 | title = "TornadoFX OSGi Console" 17 | prefWidth = 800.0 18 | prefHeight = 600.0 19 | 20 | center { 21 | tableview { 22 | columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY 23 | 24 | column("ID", Long::class) { 25 | value { it.value.bundleId } 26 | fixedWidth(75.0) 27 | } 28 | 29 | column("State", String::class) { 30 | value { it.value.stateDescription } 31 | fixedWidth(120.0) 32 | } 33 | column("Level", Int::class) { 34 | value { it.value.state } 35 | fixedWidth(50.0) 36 | } 37 | column("Name", String::class) { 38 | value { it.value.description } 39 | } 40 | 41 | items.setAll(fxBundleContext.bundles.toList()) 42 | 43 | fxBundleContext.addBundleListener { 44 | items.setAll(fxBundleContext.bundles.toList()) 45 | } 46 | 47 | contextmenu { 48 | item("Stop").action { 49 | selectedItem?.stop() 50 | } 51 | item("Start").action { 52 | selectedItem?.start() 53 | } 54 | item("Uninstall").action { 55 | selectedItem?.uninstall() 56 | } 57 | item("Update").action { 58 | selectedItem?.update() 59 | } 60 | item("Update from...").action { 61 | val result = chooseFile("Select file to replace ${selectedItem!!.symbolicName}", arrayOf(FileChooser.ExtensionFilter("OSGi Bundle Jar", "jar"))) 62 | if (result.isNotEmpty()) selectedItem?.update(Files.newInputStream(result.first().toPath())) 63 | } 64 | item("Set start level...").action { 65 | TextInputDialog("").showAndWait().ifPresent { 66 | selectedItem!!.bundleContext.bundle.adapt(BundleStartLevel::class.java).startLevel = it.toInt() 67 | } 68 | } 69 | } 70 | 71 | setOnContextMenuRequested { 72 | val stop = contextMenu.items.first { it.text == "Stop" } 73 | val start = contextMenu.items.first { it.text == "Start" } 74 | stop.isDisable = selectedItem?.state != Bundle.ACTIVE 75 | start.isDisable = !stop.isDisable 76 | } 77 | 78 | setOnDragOver { event -> 79 | if (event.dragboard.hasFiles()) event.acceptTransferModes(TransferMode.COPY) 80 | event.consume() 81 | } 82 | 83 | setOnDragDropped { event -> 84 | if (event.dragboard.hasFiles() && event.dragboard.files.first().name.toLowerCase().endsWith(".jar")) { 85 | event.dragboard.files.forEach { 86 | fxBundleContext.installBundle("file:${it.absolutePath}") 87 | } 88 | event.isDropCompleted = true 89 | } else { 90 | event.isDropCompleted = false 91 | } 92 | event.consume() 93 | } 94 | } 95 | } 96 | 97 | } 98 | 99 | val Bundle.stateDescription : String get() = when (state) { 100 | ACTIVE -> "Active" 101 | Bundle.INSTALLED -> "Installed" 102 | Bundle.RESOLVED -> "Resolved" 103 | Bundle.STARTING -> "Starting" 104 | Bundle.STOPPING -> "Stopping" 105 | Bundle.UNINSTALLED -> "Uninstalled" 106 | else -> "Unknown" 107 | } 108 | 109 | val Bundle.description : String get() { 110 | val name = headers["Bundle-Name"] ?: symbolicName ?: location ?: "?" 111 | return "$name | $version" 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/StylesheetProvider.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi 2 | 3 | import org.osgi.framework.BundleContext 4 | import tornadofx.Stylesheet 5 | import java.util.* 6 | import kotlin.reflect.KClass 7 | 8 | interface StylesheetProvider { 9 | val stylesheet: KClass 10 | } 11 | 12 | inline fun BundleContext.registerStylesheet() = registerStylesheet(T::class) 13 | fun BundleContext.registerStylesheet(stylesheet: KClass) { 14 | val provider = object : StylesheetProvider { 15 | override val stylesheet = stylesheet 16 | } 17 | registerService(StylesheetProvider::class.java, provider, Hashtable()) 18 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/ViewProvider.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi 2 | 3 | import javafx.application.Platform 4 | import javafx.event.EventTarget 5 | import org.osgi.framework.BundleContext 6 | import org.osgi.framework.FrameworkUtil 7 | import tornadofx.FX 8 | import tornadofx.UIComponent 9 | import tornadofx.find 10 | import tornadofx.plusAssign 11 | import java.util.* 12 | import kotlin.reflect.KClass 13 | 14 | interface ViewProvider { 15 | fun getView(): UIComponent 16 | val discriminator: Any? 17 | } 18 | 19 | interface ViewReceiver { 20 | fun viewProvided(provider: ViewProvider) 21 | } 22 | 23 | 24 | /** 25 | * Provide this View to other OSGi Bundles. To receive this View, call `Node.addViewWhen` on the containing Node 26 | */ 27 | fun BundleContext.registerView(viewType: KClass, discriminator: Any? = null) { 28 | val provider = object : ViewProvider { 29 | override fun getView() = find(viewType) 30 | override val discriminator = discriminator 31 | } 32 | registerService(ViewProvider::class.java, provider, Hashtable()) 33 | } 34 | inline fun BundleContext.registerView(discriminator: Any? = null) = registerView(T::class, discriminator) 35 | 36 | /** 37 | * Subscribe to ViewProvider events from other OSGi bundles and 38 | * add the provided view to this UI element if the acceptor returns true. 39 | */ 40 | fun EventTarget.addViewsWhen(acceptor: (ViewProvider) -> Boolean) { 41 | require(FX.osgiAvailable) { "You can only subscribe to ViewProviders when you're in an OSGi context" } 42 | val context = FrameworkUtil.getBundle(acceptor.javaClass).bundleContext 43 | val receiver = object : ViewReceiver { 44 | override fun viewProvided(provider: ViewProvider) { 45 | Platform.runLater { 46 | if (acceptor.invoke(provider)) 47 | plusAssign(provider.getView()) 48 | } 49 | } 50 | } 51 | context.registerService(ViewReceiver::class.java, receiver, Hashtable()) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/Activator.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi.impl 2 | 3 | import org.osgi.framework.BundleActivator 4 | import org.osgi.framework.BundleContext 5 | import org.osgi.service.url.URLStreamHandlerService 6 | import java.util.* 7 | 8 | internal class Activator : BundleActivator { 9 | lateinit var applicationListener: ApplicationListener 10 | lateinit var stylesheetListener: StylesheetListener 11 | lateinit var viewListener: ViewListener 12 | lateinit var interceptorListener: InterceptorListener 13 | override fun start(context: BundleContext) { 14 | applicationListener = ApplicationListener(context) 15 | stylesheetListener = StylesheetListener(context) 16 | viewListener = ViewListener(context) 17 | interceptorListener = InterceptorListener(context) 18 | context.addServiceListener(applicationListener) 19 | context.addServiceListener(stylesheetListener) 20 | context.addServiceListener(viewListener) 21 | context.addServiceListener(interceptorListener) 22 | val cssOptions = Hashtable() 23 | cssOptions["url.handler.protocol"] = "css" 24 | cssOptions["url.content.mimetype"] = "text/css" 25 | context.registerService(URLStreamHandlerService::class.java, CSSURLStreamHandlerService(), cssOptions) 26 | } 27 | 28 | override fun stop(context: BundleContext) { 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/CSSURLStreamHandlerService.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi.impl 2 | 3 | import org.osgi.service.url.URLStreamHandlerService 4 | import org.osgi.service.url.URLStreamHandlerSetter 5 | import tornadofx.* 6 | import java.io.InputStream 7 | import java.net.URL 8 | import java.net.URLConnection 9 | import java.net.URLStreamHandler 10 | import java.nio.charset.StandardCharsets 11 | import java.util.* 12 | 13 | internal class CSSURLStreamHandlerService : URLStreamHandler(), URLStreamHandlerService { 14 | private lateinit var realHandler: URLStreamHandlerSetter 15 | 16 | override fun openConnection(url: URL): URLConnection = CSSURLConnection(url) 17 | 18 | class CSSURLConnection(url: URL) : URLConnection(url) { 19 | override fun connect() { 20 | } 21 | 22 | override fun getInputStream(): InputStream { 23 | if (url.port == 64) return Base64.getDecoder().decode(url.host).inputStream() 24 | val owningBundle = fxBundleContext.getBundle(url.query.substringBefore("&").substringBefore("?").toLong())!! 25 | val stylesheet = owningBundle.loadClass(url.host).newInstance() as Stylesheet 26 | val rendered = stylesheet.render() 27 | if (FX.dumpStylesheets) println(rendered) 28 | return rendered.byteInputStream(StandardCharsets.UTF_8) 29 | } 30 | } 31 | 32 | override fun parseURL(realHandler: URLStreamHandlerSetter, u: URL, spec: String, start: Int, limit: Int) { 33 | this.realHandler = realHandler 34 | parseURL(u, spec, start, limit) 35 | } 36 | 37 | override fun setURL(u: URL?, protocol: String?, host: String?, port: Int, authority: String?, userInfo: String?, path: String?, query: String?, ref: String?) { 38 | realHandler.setURL(u, protocol, host, port, authority, userInfo, path, query, ref) 39 | } 40 | 41 | override fun hashCode(u: URL) = super.hashCode(u) 42 | override fun toExternalForm(u: URL?) = super.toExternalForm(u) 43 | override fun equals(u1: URL?, u2: URL?) = super.equals(u1, u2) 44 | override fun sameFile(u1: URL?, u2: URL?) = super.sameFile(u1, u2) 45 | override fun getDefaultPort() = super.getDefaultPort() 46 | override fun getHostAddress(u: URL?) = super.getHostAddress(u) 47 | override fun hostsEqual(u1: URL?, u2: URL?) = super.hostsEqual(u1, u2) 48 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/InterceptorListener.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi.impl 2 | 3 | import org.osgi.framework.BundleContext 4 | import org.osgi.framework.ServiceEvent 5 | import org.osgi.framework.ServiceListener 6 | import org.osgi.util.tracker.ServiceTracker 7 | import tornadofx.* 8 | import tornadofx.osgi.ChildInterceptorProvider 9 | 10 | class InterceptorListener(val context: BundleContext) : ServiceListener { 11 | val tracker = ServiceTracker(context, context.createFilter("(&(objectClass=${ChildInterceptorProvider::class.java.name}))"), null) 12 | 13 | init { 14 | tracker.open() 15 | tracker.withEach { 16 | FX.addChildInterceptor(it.interceptor) 17 | } 18 | } 19 | 20 | override fun serviceChanged(event: ServiceEvent) { 21 | if (event.isChildInterceptorProviderEvent()) { 22 | val provider = context.getService(event.serviceReference) as ChildInterceptorProvider 23 | 24 | if (event.type == ServiceEvent.REGISTERED) { 25 | FX.addChildInterceptor(provider.interceptor) 26 | } else if (event.type == ServiceEvent.UNREGISTERING) { 27 | FX.removeChildInterceptor(provider.interceptor) 28 | } 29 | } 30 | } 31 | 32 | private fun ServiceEvent.isChildInterceptorProviderEvent() = objectClass == ChildInterceptorProvider::class.qualifiedName 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/OSGISupport.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package tornadofx.osgi.impl 4 | 5 | import org.osgi.framework.Bundle 6 | import org.osgi.framework.BundleContext 7 | import org.osgi.framework.FrameworkUtil 8 | import org.osgi.framework.ServiceEvent 9 | import org.osgi.util.tracker.ServiceTracker 10 | import tornadofx.FX 11 | import tornadofx.withEach 12 | import java.util.logging.Level 13 | import kotlin.reflect.KClass 14 | 15 | val ServiceEvent.objectClass: String 16 | get() = (serviceReference.getProperty("objectClass") as Array)[0] 17 | 18 | val fxBundle: Bundle get() = FrameworkUtil.getBundle(Activator::class.java) 19 | val fxBundleContext: BundleContext get() = fxBundle.bundleContext 20 | 21 | /** 22 | * Try to resolve the OSGi Bundle Id of the given class. This function can only be called 23 | * if OSGi is available on the classpath. 24 | */ 25 | fun getBundleId(classFromBundle: KClass<*>): Long? { 26 | try { 27 | return FrameworkUtil.getBundle(classFromBundle.java)?.bundleId 28 | } catch (ex: Exception) { 29 | FX.log.log(Level.WARNING, "OSGi was on the classpath but no Framework did not respond correctly", ex) 30 | return null 31 | } 32 | } 33 | inline fun getBundleId(): Long? = getBundleId(T::class) 34 | 35 | inline fun ServiceTracker.withEach(fn: (S) -> Unit) { 36 | services?.withEach {fn(this as S) } 37 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/StylesheetListener.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi.impl 2 | 3 | import org.osgi.framework.BundleContext 4 | import org.osgi.framework.ServiceEvent 5 | import org.osgi.framework.ServiceListener 6 | import org.osgi.util.tracker.ServiceTracker 7 | import tornadofx.importStylesheet 8 | import tornadofx.osgi.StylesheetProvider 9 | import tornadofx.removeStylesheet 10 | 11 | internal class StylesheetListener(val context: BundleContext) : ServiceListener { 12 | val tracker = ServiceTracker(context, context.createFilter("(&(objectClass=${StylesheetProvider::class.java.name}))"), null) 13 | 14 | init { 15 | tracker.open() 16 | tracker.withEach { 17 | importStylesheet(it.stylesheet) 18 | } 19 | } 20 | 21 | override fun serviceChanged(event: ServiceEvent) { 22 | if (event.isStylesheetProviderEvent()) { 23 | val provider = context.getService(event.serviceReference) as StylesheetProvider 24 | 25 | if (event.type == ServiceEvent.REGISTERED) { 26 | importStylesheet(provider.stylesheet) 27 | } else if (event.type == ServiceEvent.UNREGISTERING) { 28 | removeStylesheet(provider.stylesheet) 29 | } 30 | } 31 | } 32 | 33 | private fun ServiceEvent.isStylesheetProviderEvent() = objectClass == StylesheetProvider::class.qualifiedName 34 | 35 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/osgi/impl/ViewListener.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.osgi.impl 2 | 3 | import javafx.application.Platform 4 | import org.osgi.framework.BundleContext 5 | import org.osgi.framework.ServiceEvent 6 | import org.osgi.framework.ServiceListener 7 | import org.osgi.util.tracker.ServiceTracker 8 | import tornadofx.osgi.ViewProvider 9 | import tornadofx.osgi.ViewReceiver 10 | import tornadofx.removeFromParent 11 | 12 | internal class ViewListener(val context: BundleContext) : ServiceListener { 13 | val providerTracker = ServiceTracker(context, context.createFilter("(&(objectClass=${ViewProvider::class.java.name}))"), null) 14 | val receiverTracker = ServiceTracker(context, context.createFilter("(&(objectClass=${ViewReceiver::class.java.name}))"), null) 15 | 16 | init { 17 | providerTracker.open() 18 | receiverTracker.open() 19 | 20 | providerTracker.withEach { provider -> 21 | receiverTracker.withEach { receiver -> 22 | Platform.runLater { 23 | receiver.viewProvided(provider) 24 | } 25 | } 26 | } 27 | 28 | } 29 | 30 | override fun serviceChanged(event: ServiceEvent) { 31 | if (event.isViewProviderEvent()) { 32 | val provider = context.getService(event.serviceReference) as ViewProvider 33 | 34 | if (event.type == ServiceEvent.REGISTERED) { 35 | receiverTracker.withEach { 36 | Platform.runLater { 37 | it.viewProvided(provider) 38 | } 39 | } 40 | } else if (event.type == ServiceEvent.UNREGISTERING) { 41 | Platform.runLater { 42 | provider.getView().root.removeFromParent() 43 | } 44 | } 45 | } else if (event.isViewReceiverEvent()) { 46 | val receiver = context.getService(event.serviceReference) as ViewReceiver 47 | 48 | if (event.type == ServiceEvent.REGISTERED) { 49 | providerTracker.withEach { 50 | Platform.runLater { 51 | receiver.viewProvided(it) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | private fun ServiceEvent.isViewProviderEvent() = objectClass == ViewProvider::class.qualifiedName 59 | private fun ServiceEvent.isViewReceiverEvent() = objectClass == ViewReceiver::class.qualifiedName 60 | } -------------------------------------------------------------------------------- /src/main/java/tornadofx/skin/tablerow/DirtyDecoratingTableRowSkin.java: -------------------------------------------------------------------------------- 1 | package tornadofx.skin.tablerow; 2 | 3 | import javafx.collections.ObservableList; 4 | import javafx.collections.ObservableMap; 5 | import javafx.scene.Node; 6 | import javafx.scene.control.TableCell; 7 | import javafx.scene.control.TableRow; 8 | import javafx.scene.control.skin.TableRowSkin; 9 | import javafx.scene.paint.Color; 10 | import javafx.scene.shape.Polygon; 11 | import tornadofx.TableViewEditModel; 12 | 13 | // This needs to be a Java class because of a Kotlin Bug: 14 | // https://youtrack.jetbrains.com/issue/KT-12255 15 | 16 | public final class DirtyDecoratingTableRowSkin extends TableRowSkin { 17 | 18 | private static final String KEY = "tornadofx.dirtyStatePolygon"; 19 | private final TableViewEditModel editModel; 20 | 21 | public DirtyDecoratingTableRowSkin(TableRow tableRow, TableViewEditModel editModel) { 22 | super(tableRow); 23 | this.editModel = editModel; 24 | } 25 | 26 | private Polygon getPolygon(TableCell cell) { 27 | ObservableMap properties = cell.getProperties(); 28 | return (Polygon) properties.computeIfAbsent(KEY, x -> { 29 | Polygon polygon = new Polygon(0.0, 0.0, 0.0, 10.0, 10.0, 0.0); 30 | polygon.setFill(Color.BLUE); 31 | return polygon; 32 | }); 33 | } 34 | 35 | protected void layoutChildren(double x, double y, double w, double h) { 36 | super.layoutChildren(x, y, w, h); 37 | final ObservableList children = getChildren(); 38 | 39 | children.forEach(child -> { 40 | TableCell cell = (TableCell) child; 41 | T item = null; 42 | if (cell.getIndex() > -1 && cell.getTableView().getItems().size() > cell.getIndex()) { 43 | item = cell.getTableView().getItems().get(cell.getIndex()); 44 | } 45 | Polygon polygon = getPolygon(cell); 46 | boolean isDirty = item != null && editModel.getDirtyState(item).isDirtyColumn(cell.getTableColumn()); 47 | if (isDirty) { 48 | if (!children.contains(polygon)) { 49 | children.add(polygon); 50 | } 51 | polygon.relocate(cell.getLayoutX(), y); 52 | } else { 53 | children.remove(polygon); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/tornadofx/skin/tablerow/ExpandableTableRowSkin.java: -------------------------------------------------------------------------------- 1 | package tornadofx.skin.tablerow; 2 | 3 | import javafx.scene.Node; 4 | import javafx.scene.control.TableRow; 5 | import javafx.scene.control.skin.TableRowSkin; 6 | import tornadofx.ExpanderColumn; 7 | 8 | // This needs to be a Java class because of a Kotlin Bug: 9 | // https://youtrack.jetbrains.com/issue/KT-12255 10 | 11 | public final class ExpandableTableRowSkin extends TableRowSkin { 12 | 13 | private double tableRowPrefHeight = -1.0D; 14 | 15 | private final TableRow tableRow; 16 | private final ExpanderColumn expander; 17 | 18 | public ExpandableTableRowSkin(TableRow tableRow, ExpanderColumn expander) { 19 | super(tableRow); 20 | this.tableRow = tableRow; 21 | this.expander = expander; 22 | this.tableRow.itemProperty().addListener((observable, oldValue, newValue) -> { 23 | if (oldValue != null) { 24 | Node expandedNode = expander.getExpandedNode(oldValue); 25 | if (expandedNode != null) { 26 | getChildren().remove(expandedNode); 27 | } 28 | } 29 | }); 30 | } 31 | 32 | private boolean getExpanded() { 33 | TableRow tableRow = getSkinnable(); 34 | final S item = tableRow.getItem(); 35 | return (item != null && expander.getCellData(getSkinnable().getIndex())); 36 | } 37 | 38 | private Node getContent() { 39 | Node node = expander.getOrCreateExpandedNode(tableRow); 40 | if (!getChildren().contains(node)) { 41 | getChildren().add(node); 42 | } 43 | return node; 44 | } 45 | 46 | protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 47 | tableRowPrefHeight = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); 48 | return (getExpanded() && getContent() != null) ? tableRowPrefHeight + getContent().prefHeight(width) : tableRowPrefHeight; 49 | } 50 | 51 | protected void layoutChildren(double x, double y, double w, double h) { 52 | super.layoutChildren(x, y, w, h); 53 | if (getExpanded() && getContent() != null) { 54 | getContent().resizeRelocate(0.0, tableRowPrefHeight, w, h - tableRowPrefHeight); 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/main/resources/tornadofx/datagrid.css: -------------------------------------------------------------------------------- 1 | .datagrid { 2 | -fx-cell-width: 150px; 3 | -fx-cell-height: 150px; 4 | -fx-horizontal-cell-spacing: 8px; 5 | -fx-vertical-cell-spacing: 8px; 6 | -fx-focus-traversable: true; 7 | } 8 | 9 | .datagrid-cell { 10 | -fx-background: -fx-control-inner-background; 11 | -fx-background-color: -fx-background; 12 | -fx-text-fill: -fx-text-background-color; 13 | } 14 | 15 | .datagrid-cell:selected { 16 | -fx-background: -fx-selection-bar; 17 | -fx-table-cell-border-color: derive(-fx-selection-bar, 20%); 18 | } 19 | 20 | .datagrid-cell .label { 21 | -fx-font-weight: bold; 22 | -fx-font-size: 16px; 23 | } -------------------------------------------------------------------------------- /src/main/resources/tornadofx/form.css: -------------------------------------------------------------------------------- 1 | .form { 2 | -fx-padding: 10; 3 | } 4 | 5 | .form .fieldset .label-container { 6 | -fx-alignment: center-left; 7 | } 8 | 9 | .form .fieldset:horizontal .label-container { 10 | -fx-padding: 0 15 0 0; 11 | } 12 | 13 | .form .fieldset:vertical .label-container { 14 | -fx-padding: 0 0 3 0; 15 | } 16 | 17 | .form .fieldset .input-container:horizontal { 18 | -fx-alignment: baseline-left; 19 | } 20 | 21 | .form .fieldset { 22 | -fx-padding: 0 0 15 0; 23 | } 24 | 25 | .form .input-container { 26 | -fx-spacing: 10; 27 | } 28 | 29 | .form .field { 30 | -fx-padding: 5 0; 31 | } 32 | 33 | .form .fieldset .legend { 34 | -fx-font-weight: bold; 35 | -fx-font-size: 1.2em; 36 | -fx-padding: 0 0 5 0; 37 | } 38 | 39 | .form .fieldset .legend { 40 | -fx-graphic-text-gap: 10; 41 | } -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel.properties: -------------------------------------------------------------------------------- 1 | required=This field is required -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_de_DE.properties: -------------------------------------------------------------------------------- 1 | required=Dieses Feld ist erforderlich -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_es.properties: -------------------------------------------------------------------------------- 1 | required=Este campo es obligatorio 2 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_nb_NO.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edvin/tornadofx2/17c1da828586b0cba09a6aa3a35a8c13a4a9d956/src/main/resources/tornadofx/i18n/ViewModel_nb_NO.properties -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_nl_NL.properties: -------------------------------------------------------------------------------- 1 | required=Dit veld is verplicht -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_ru_RU.properties: -------------------------------------------------------------------------------- 1 | required=\u042D\u0442\u043E \u043F\u043E\u043B\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E 2 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/ViewModel_zh_CN.properties: -------------------------------------------------------------------------------- 1 | required=\u6b64\u5b57\u6bb5\u4e3a\u5fc5\u586b\u5b57\u6bb5 -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard.properties: -------------------------------------------------------------------------------- 1 | back=< _Back 2 | cancel=_Cancel 3 | finish=_Finish 4 | next=_Next > 5 | steps=Steps -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_de_DE.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edvin/tornadofx2/17c1da828586b0cba09a6aa3a35a8c13a4a9d956/src/main/resources/tornadofx/i18n/Wizard_de_DE.properties -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_es.properties: -------------------------------------------------------------------------------- 1 | back=< _Atr\u00e1s 2 | cancel=_Cancelar 3 | finish=_Finalizar 4 | next=_Siguiente > 5 | steps=Pasos 6 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_nb_NO.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edvin/tornadofx2/17c1da828586b0cba09a6aa3a35a8c13a4a9d956/src/main/resources/tornadofx/i18n/Wizard_nb_NO.properties -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_nl_NL.properties: -------------------------------------------------------------------------------- 1 | back=< _Terug 2 | cancel=_Annuleren 3 | finish=_Voltooien 4 | next=_Volgende > 5 | steps=Stappen -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_ru_RU.properties: -------------------------------------------------------------------------------- 1 | back=< _\u041D\u0430\u0437\u0430\u0434 2 | cancel=_\u041E\u0442\u043C\u0435\u043D\u0430 3 | finish=_\u0413\u043E\u0442\u043E\u0432\u043E 4 | next=_\u0414\u0430\u043B\u0435\u0435 > 5 | steps=\u0428\u0430\u0433\u0438 6 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/i18n/Wizard_zh_CN.properties: -------------------------------------------------------------------------------- 1 | back=< _\u540e\u9000 2 | cancel=_\u53d6\u6d88 3 | finish=_\u5b8c\u6210 4 | next=_\u4e0b\u4e00\u4e2a > 5 | steps=\u6b65\u9aa4 -------------------------------------------------------------------------------- /src/main/resources/tornadofx/listmenu.css: -------------------------------------------------------------------------------- 1 | .list-menu { 2 | -fx-graphic-fixed-size: 2em; 3 | } 4 | 5 | .list-menu .list-item { 6 | -fx-cursor: hand; 7 | -fx-padding: 10; 8 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; 9 | -fx-background-insets: 0 0 -0.5 0, 0, 0.5, 1.5; 10 | } 11 | 12 | .list-menu .list-item .label { 13 | -fx-text-fill: -fx-text-base-color; 14 | } 15 | 16 | .list-menu .list-item:active { 17 | -fx-background-color: -fx-focus-color, -fx-inner-border, -fx-body-color, -fx-faint-focus-color, -fx-body-color; 18 | -fx-background-insets: -0.2, 1, 2, -1.4, 2.6; 19 | } 20 | 21 | .list-menu .list-item:hover { 22 | -fx-color: -fx-hover-base; 23 | } 24 | 25 | .list-menu .list-item:disabled { 26 | -fx-opacity: 0.4; 27 | } 28 | 29 | .list-menu.blue { 30 | -fx-graphic-fixed-size: 2.5em; 31 | -fx-background-color: #1e88cf; 32 | } 33 | 34 | .list-menu.blue .list-item { 35 | -fx-background-color: #1e88cf; 36 | } 37 | 38 | .list-menu.blue .list-item * { 39 | -fx-fill: #fff; 40 | } 41 | 42 | .list-menu.blue .list-item:hover .graphic { 43 | -fx-fill: #ddd; 44 | } 45 | 46 | .list-menu.blue .list-item:active { 47 | -fx-background-color: #3da0e3; 48 | } 49 | 50 | .list-menu.blue .list-item:active:hover .graphic { 51 | -fx-fill: #fff; 52 | } -------------------------------------------------------------------------------- /src/main/resources/tornadofx/maskpane.css: -------------------------------------------------------------------------------- 1 | .mask-pane { 2 | -fx-background-color: rgba(0,0,0,0.5); 3 | -fx-accent: aliceblue; 4 | } 5 | 6 | .mask-pane > .progress-indicator { 7 | -fx-max-width: 300; 8 | -fx-max-height: 300; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/rowexpanderpane.css: -------------------------------------------------------------------------------- 1 | .table-view .column-header .label, .tree-table-view .column-header .label { 2 | -fx-text-fill: -fx-dark-text-color; 3 | } 4 | -------------------------------------------------------------------------------- /src/main/resources/tornadofx/workspace.css: -------------------------------------------------------------------------------- 1 | .workspace .header { 2 | -fx-spacing: 0.4em; 3 | } 4 | 5 | .workspace .header .heading-container { 6 | -fx-font-weight: bold; 7 | -fx-font-size: 1.2em; 8 | -fx-alignment: center-left; 9 | -fx-padding: 0 0 0 0.2em; 10 | -fx-spacing: 1em; 11 | } 12 | 13 | .workspace .header .heading-container .text { 14 | -fx-fill: #5f5f5f; 15 | } 16 | 17 | .workspace .header .container { 18 | -fx-padding: 0 0.2em 0 0.2em; 19 | -fx-alignment: center-left; 20 | } 21 | 22 | .workspace .header .button.icon-only { 23 | -fx-background-color: transparent; 24 | -fx-background-insets: 0; 25 | -fx-background-radius: 3px; 26 | -fx-padding: 0.333333em; 27 | } 28 | 29 | .workspace .header .button.icon-only:hover { 30 | -fx-background-color: #e3e3e3; 31 | } 32 | 33 | .workspace .header .button.icon-only:armed { 34 | -fx-background-color: #e3e3e3; 35 | -fx-background-radius: 1em; 36 | } 37 | 38 | .workspace .header .icon { 39 | -fx-min-width: 16px; 40 | -fx-min-height: 16px; 41 | -fx-max-width: 16px; 42 | -fx-max-height: 16px; 43 | -fx-background-color: #818181; 44 | } 45 | 46 | .workspace .header .icon.back { 47 | -fx-shape: "M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" 48 | } 49 | 50 | .workspace .header .icon.forward { 51 | -fx-shape: "M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" 52 | } 53 | 54 | .workspace .header .icon.refresh { 55 | -fx-shape: "M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" 56 | } 57 | 58 | .workspace .header .icon.save { 59 | -fx-shape: "M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" 60 | } 61 | 62 | .workspace .header .icon.create { 63 | -fx-shape: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" 64 | } 65 | 66 | .workspace .header .icon.delete { 67 | -fx-shape: "M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" 68 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/AccelTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.control.Alert 4 | import javafx.scene.input.KeyCode 5 | import javafx.scene.input.KeyCodeCombination 6 | import javafx.scene.input.KeyCombination 7 | import tornadofx.* 8 | 9 | class AccelTestApp : App(AccelTest::class) 10 | 11 | class AccelTest : View() { 12 | override val root = stackpane { 13 | add() 14 | } 15 | } 16 | 17 | class AccelView : View() { 18 | override val root = button("Click me") { 19 | shortcut(KeyCodeCombination(KeyCode.A, KeyCombination.SHORTCUT_ANY, KeyCombination.SHIFT_ANY)) 20 | action { 21 | alert(Alert.AlertType.INFORMATION, "Fire!", "You clicked.") 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/AnimationHelperTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.animation.Animation 4 | import javafx.animation.Interpolator 5 | import javafx.scene.paint.Color 6 | import tornadofx.* 7 | 8 | class ShapeTransitionTest : App(Main::class) { 9 | class Main : View("Shape Transition Test") { 10 | var switch = true 11 | val nextFill get() = if (switch) c(1.0, 0.0, 0.0) else c(0.0, 0.0, 1.0) 12 | val nextStroke get() = if (switch) c(0.0, 0.0, 1.0) else c(1.0, 0.0, 0.0) 13 | override val root = stackpane { 14 | background = Color.BLACK.asBackground() 15 | val round = circle(radius = 100.0) { 16 | fill = nextFill 17 | stroke = nextStroke 18 | switch = !switch 19 | strokeWidth = 20.0 20 | } 21 | val orbit = circle(0.0, 0.0, 10.0) { 22 | animateFill(0.33.seconds, Color.ORANGE, Color.YELLOW) { 23 | isAutoReverse = true 24 | cycleCount = Animation.INDEFINITE 25 | } 26 | }.follow(3.seconds, round, Interpolator.LINEAR) { cycleCount = Animation.INDEFINITE } 27 | button("Click Me") { 28 | action { 29 | isDisable = true 30 | val startFill = round.fill as? Color ?: Color.BLACK 31 | val endFill = nextFill 32 | val startStroke = round.stroke as? Color ?: Color.BLACK 33 | val endStroke = nextStroke 34 | switch = !switch 35 | listOf( 36 | pause(0.75.seconds, play = false) { setOnFinished { orbit.rate = -orbit.rate } }, 37 | listOf( 38 | round.animateFill(0.25.seconds, startFill, endFill, play = false), 39 | round.animateStroke(0.25.seconds, startStroke, endStroke, play = false) 40 | ).playParallel(false) 41 | ).playSequential { setOnFinished { isDisable = false } } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/AsyncProgressApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.paint.Color 4 | import javafx.scene.text.FontWeight 5 | import tornadofx.* 6 | 7 | class AsyncProgressApp : App(AsyncProgressView::class) 8 | 9 | class AsyncProgressView : View("Async Progress") { 10 | override val root = borderpane { 11 | setPrefSize(400.0, 300.0) 12 | 13 | center { 14 | button("Start") { 15 | action { 16 | runAsync { 17 | updateTitle("Doing some work") 18 | for (i in 1..10) { 19 | updateMessage("Working $i...") 20 | if (i == 5) 21 | updateTitle("Dome something else") 22 | Thread.sleep(200) 23 | updateProgress(i.toLong(), 10) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | bottom { 30 | add() 31 | } 32 | } 33 | } 34 | 35 | class ProgressView : View() { 36 | val status: TaskStatus by inject() 37 | 38 | override val root = vbox(4) { 39 | visibleWhen { status.running } 40 | style { borderColor += box(Color.LIGHTGREY, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) } 41 | label(status.title).style { fontWeight = FontWeight.BOLD } 42 | hbox(4) { 43 | label(status.message) 44 | progressbar(status.progress) 45 | visibleWhen { status.running } 46 | } 47 | } 48 | } 49 | 50 | class AsyncProgressButtonView : View() { 51 | override val root = stackpane { 52 | setPrefSize(400.0, 400.0) 53 | button("Click me") { 54 | action { 55 | runAsyncWithProgress { 56 | Thread.sleep(2000) 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/AwaitUntilTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import javafx.beans.property.SimpleObjectProperty 5 | import javafx.beans.property.SimpleStringProperty 6 | import javafx.util.converter.IntegerStringConverter 7 | import tornadofx.* 8 | import java.time.LocalDateTime 9 | 10 | class AwaitUntilTest : View("Await Until Test") { 11 | val number = SimpleObjectProperty() 12 | val flag = SimpleBooleanProperty(false) 13 | val output = SimpleStringProperty("") 14 | 15 | override val root = borderpane { 16 | top { 17 | toolbar { 18 | label("Number:") 19 | textfield(number, IntegerStringConverter()) 20 | button("Wait until 42").action { 21 | number.awaitUntil { it == 42 } 22 | output.value += "Number is now 42\n" 23 | } 24 | button("Wait until true").action { 25 | runLater(2.seconds) { 26 | flag.set(true) 27 | } 28 | flag.awaitUntil() 29 | output.value += "Flag is true\n" 30 | } 31 | button("Look alive").action { 32 | output.value += "Alive at ${LocalDateTime.now()}\n" 33 | } 34 | } 35 | } 36 | center { 37 | textarea(output) 38 | } 39 | } 40 | } 41 | 42 | class AwaitUntilApp : App(AwaitUntilTest::class) 43 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/BorderPaneTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class BorderPaneTestApp : App(BorderPaneTest::class) 6 | 7 | class BorderPaneTest : View("Border Pane Builder Test") { 8 | override val root = borderpane { 9 | // Direct access 10 | top = label("Top") 11 | // Builder target 12 | center { 13 | hbox { 14 | label("Center") 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/BrokenLineTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.layout.BorderStrokeStyle 4 | import javafx.scene.shape.StrokeLineCap 5 | import javafx.scene.shape.StrokeLineJoin 6 | import javafx.scene.shape.StrokeType 7 | import tornadofx.* 8 | 9 | class BrokenLineApp : App(BrokenLineView::class, BrokenStyles::class) 10 | class BrokenLineView : View("Broken Line Test") { 11 | override val root = stackpane { 12 | line { 13 | addClass(BrokenStyles.blue) 14 | endXProperty().bind(this@stackpane.widthProperty()) 15 | endYProperty().bind(this@stackpane.heightProperty()) 16 | } 17 | line { 18 | addClass(BrokenStyles.blue) 19 | endXProperty().bind(this@stackpane.widthProperty()) 20 | startYProperty().bind(this@stackpane.heightProperty()) 21 | } 22 | label("This is a label with a border").addClass(BrokenStyles.red) 23 | } 24 | } 25 | 26 | class BrokenStyles : Stylesheet() { 27 | companion object { 28 | val red by cssclass() 29 | val blue by cssclass() 30 | } 31 | 32 | init { 33 | root { 34 | padding = box(25.px) 35 | backgroundColor += c("white") 36 | } 37 | red { 38 | val radius = box(50.px) 39 | backgroundColor += c("white", 0.9) 40 | backgroundRadius += radius 41 | borderColor += box(c("red")) 42 | borderWidth += box(5.px) 43 | borderRadius += radius 44 | borderStyle += BorderStrokeStyle( 45 | StrokeType.CENTERED, 46 | StrokeLineJoin.MITER, 47 | StrokeLineCap.SQUARE, 48 | 10.0, 49 | 0.0, 50 | listOf(5.0, 15.0, 0.0, 15.0) 51 | ) 52 | padding = box(50.px) 53 | } 54 | blue { 55 | stroke = c("dodgerblue") 56 | strokeWidth = 5.px 57 | strokeType = StrokeType.CENTERED 58 | strokeLineCap = StrokeLineCap.SQUARE 59 | strokeDashArray = listOf(25.px, 15.px, 0.px, 15.px) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/BuilderWindowTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.application.Platform 4 | import javafx.scene.paint.Color 5 | import javafx.scene.text.FontWeight 6 | import javafx.stage.StageStyle.DECORATED 7 | import javafx.stage.StageStyle.UNDECORATED 8 | import tornadofx.* 9 | 10 | class BuilderWindowTestApp : App(DangerButtonView::class) 11 | 12 | class DangerButtonView : View("Do not click the button!") { 13 | override val root = stackpane { 14 | setPrefSize(400.0, 150.0) 15 | hbox { 16 | button("Don't click me") { 17 | style { 18 | fontSize = 20.px 19 | fontWeight = FontWeight.BOLD 20 | textFill = Color.RED 21 | } 22 | action { 23 | builderWindow("What do you want?", stageStyle = UNDECORATED, owner = primaryStage) { 24 | vbox(10) { 25 | style { 26 | padding = box(20.px) 27 | borderColor += box(Color.ORANGE) 28 | borderWidth += box(2.px) 29 | } 30 | 31 | label("So, you clicked it anyway.. What do you want?") { 32 | style { 33 | fontWeight = FontWeight.BOLD 34 | } 35 | } 36 | 37 | hbox(10) { 38 | button("Tell them you clicked").action { 39 | this@DangerButtonView.title = "It's not dangerous to click the button :)" 40 | close() 41 | } 42 | button("Close app").action { 43 | Platform.exit() 44 | } 45 | button("Cancel").action { 46 | close() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | button("the same in internalWindow").action { 54 | openInternalBuilderWindow("Internal window", modal = true, overlayPaint = Color.DARKRED) { 55 | vbox(20) { 56 | label("opened in an internalwindow") 57 | button("close").action { close() } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ChartTestApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.chart.CategoryAxis 4 | import javafx.scene.chart.NumberAxis 5 | import javafx.scene.paint.Color 6 | import tornadofx.* 7 | 8 | class BarChartTestApp : App(BarChartTest::class, BarChartStyles::class) 9 | 10 | class BarChartTest : View() { 11 | override val root = barchart("Stock Monitoring, 2010", CategoryAxis(), NumberAxis()) { 12 | series("Portfolio 1") { 13 | data("Jan", 23) 14 | data("Feb", 14) 15 | data("Mar", 15) 16 | } 17 | series("Portfolio 2") { 18 | data("Jan", 11) 19 | data("Feb", 19) 20 | data("Mar", 27) 21 | } 22 | } 23 | } 24 | 25 | class BarChartStyles : Stylesheet() { 26 | val chartBar by cssclass() 27 | 28 | init { 29 | defaultColor0 and chartBar { 30 | barFill = Color.VIOLET 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/CommandTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleDoubleProperty 4 | import javafx.beans.property.SimpleIntegerProperty 5 | import javafx.beans.property.SimpleStringProperty 6 | import javafx.geometry.Pos 7 | import javafx.scene.control.Alert 8 | import javafx.scene.control.Alert.AlertType.INFORMATION 9 | import tornadofx.* 10 | 11 | class CommandTestApp : App(CommandTest::class) 12 | 13 | class CommandTest : View("Command test") { 14 | val ctrl: CommandController by inject() 15 | val nameProperty = SimpleStringProperty() 16 | val numberProperty = SimpleDoubleProperty() 17 | 18 | override val root = borderpane { 19 | top { 20 | menubar { 21 | menu("Talk") { 22 | item("Say hello").command = ctrl.helloCommand 23 | customitem { 24 | vbox { 25 | text("I'm custom!") 26 | } 27 | action { 28 | println("Custom item clicked!") 29 | } 30 | } 31 | } 32 | } 33 | } 34 | center { 35 | vbox(20) { 36 | minWidth = 600.0 37 | paddingAll = 50 38 | 39 | hbox(10) { 40 | alignment = Pos.CENTER 41 | label("Name:") 42 | textfield(nameProperty) 43 | 44 | button("Say hello") { 45 | command = ctrl.helloCommand with nameProperty 46 | } 47 | 48 | button("Call action off of UI thread").action { 49 | runAsync { 50 | ctrl.helloCommand.execute("Some Name") 51 | } 52 | } 53 | } 54 | hbox(10) { 55 | alignment = Pos.CENTER 56 | label("Enter number:") 57 | textfield(numberProperty) 58 | hyperlink("Calculate square root") { 59 | command = ctrl.squareRootCommand with numberProperty 60 | } 61 | label("Square root:") 62 | label(ctrl.squareRootResult) 63 | } 64 | vbox(10) { 65 | alignment = Pos.CENTER 66 | button("Download file") { 67 | command = ctrl.downloadCommand 68 | } 69 | progressbar(ctrl.downloadProgress) { 70 | visibleWhen { progressProperty().greaterThan(0) } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | class CommandController : Controller() { 79 | val helloCommand = command(this::hello, ui = true, nullable = true) 80 | val squareRootCommand = command(this::squareRoot) 81 | val downloadCommand = command(this::download, async = true) 82 | 83 | val squareRootResult = SimpleIntegerProperty() 84 | val downloadProgress = SimpleDoubleProperty() 85 | 86 | private fun hello(name: String?) { 87 | Alert(INFORMATION, "Hello, ${name ?: "there"}").showAndWait() 88 | } 89 | 90 | private fun squareRoot(value: Double) { 91 | squareRootResult.value = Math.sqrt(value).toInt() 92 | } 93 | 94 | private fun download() { 95 | for (i in 1..100) { 96 | downloadProgress.value = i / 100.0 97 | Thread.sleep(50) 98 | } 99 | Thread.sleep(1000) 100 | downloadProgress.value = 0.0 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/DataGridTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.FXCollections 4 | import javafx.scene.Parent 5 | import javafx.scene.control.SelectionMode 6 | import tornadofx.* 7 | import tornadofx.testapps.DataGridTestApp.Companion.images 8 | 9 | class DataGridTestApp : App(DataGridTest::class, DataGridStyles::class) { 10 | companion object { 11 | val images = mapOf( 12 | "kittens" to listOf( 13 | "http://i.imgur.com/DuFZ6PQb.jpg", 14 | "http://i.imgur.com/o2QoeNnb.jpg", 15 | "http://i.imgur.com/P8wpBvub.jpg", 16 | "http://i.imgur.com/a2NDDglb.jpg", 17 | "http://i.imgur.com/79C37Scb.jpg", 18 | "http://i.imgur.com/N0fPCU2b.jpg", 19 | "http://i.imgur.com/qRfWC9wb.jpg", 20 | "http://i.imgur.com/i3crLYcb.jpg"), 21 | 22 | "puppies" to listOf( 23 | "http://i.imgur.com/37hCL2Pb.jpg", 24 | "http://i.imgur.com/v9vBE67b.jpg", 25 | "http://i.imgur.com/koQoEExb.jpg", 26 | "http://i.imgur.com/qRfWC9wb.jpg", 27 | "http://i.imgur.com/ftDV8oJb.jpg") 28 | 29 | ) 30 | } 31 | } 32 | 33 | class DataGridTest : View("DataGrid") { 34 | var datagrid: DataGrid by singleAssign() 35 | val list = FXCollections.observableArrayList() 36 | var paginator = DataGridPaginator(list, itemsPerPage = 5) 37 | 38 | override val root = borderpane { 39 | left { 40 | vbox { 41 | combobox(values = images.keys.toList()) { 42 | promptText = "Select images" 43 | valueProperty().onChange { 44 | list.setAll(images[it]) 45 | } 46 | shortcut("k") { value = "kittens" } 47 | shortcut("p") { value = "puppies" } 48 | } 49 | button("Add").action { 50 | list.add("http://i.imgur.com/bvqTBT0b.jpg") 51 | } 52 | } 53 | } 54 | center { 55 | datagrid = datagrid(paginator.items) { 56 | setPrefSize(550.0, 550.0) 57 | 58 | selectionModel.selectionMode = SelectionMode.SINGLE 59 | cellWidth = 164.0 60 | cellHeight = 164.0 61 | 62 | cellCache { 63 | imageview(it, true) 64 | } 65 | 66 | onUserSelect(1) { 67 | println("Selected $it") 68 | } 69 | } 70 | } 71 | bottom { 72 | vbox { 73 | add(paginator) 74 | hbox { 75 | label(stringBinding(datagrid.selectionModel.selectedItems) { joinToString(", ") }) 76 | button("Remove from index 2").action { 77 | if (list.size > 2) list.removeAt(2) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | class DataGridStyles : Stylesheet() { 86 | init { 87 | datagridCell and selected { 88 | opacity = 0.7 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/DrawerTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleStringProperty 4 | import javafx.scene.paint.Color 5 | import tornadofx.* 6 | 7 | class DrawerTestApp : App(DrawerWorkspace::class) { 8 | override fun onBeforeShow(view: UIComponent) { 9 | workspace.root.setPrefSize(600.0, 400.0) 10 | workspace.dock() 11 | } 12 | } 13 | 14 | class JustAView : View("Just A View") { 15 | override val root = vbox { 16 | label("I'm just a view - I do nothing") 17 | button("Load another View").action { 18 | workspace.dock() 19 | } 20 | } 21 | } 22 | 23 | class AnotherView : View("Another") { 24 | override val root = stackpane { 25 | label(title) 26 | } 27 | } 28 | 29 | class YetAnotherView : View("Yet Another") { 30 | override val root = stackpane { 31 | label(title) 32 | } 33 | } 34 | 35 | class TestDrawerContributor : View("Test View with dynamic drawer item") { 36 | override val root = stackpane { 37 | vbox { 38 | label("I add something to the drawer when I'm docked") 39 | button("Load another View").action { 40 | workspace.dock() 41 | } 42 | button("Load yet another View").action { 43 | workspace.dock() 44 | } 45 | } 46 | } 47 | 48 | override fun onDock() { 49 | workspace.leftDrawer.item("Temp Drawer") { 50 | stackpane { 51 | label("I'm only guest starring!") 52 | } 53 | } 54 | } 55 | } 56 | 57 | class DrawerWorkspace : Workspace("Drawer Workspace", Workspace.NavigationMode.Stack) { 58 | init { 59 | menubar { 60 | menu("Options") { 61 | checkmenuitem("Toggle Navigation Mode").action { 62 | navigationMode = if (navigationMode == NavigationMode.Stack) NavigationMode.Tabs else NavigationMode.Stack 63 | } 64 | } 65 | } 66 | with(bottomDrawer) { 67 | item("Console") { 68 | style { 69 | backgroundColor += Color.BLACK 70 | } 71 | label("""Connected to the target VM, address: '127.0.0.1:64653', transport: 'socket' 72 | Disconnected from the target VM, address: '127.0.0.1:64653', transport: 'socket' 73 | 74 | Process finished with exit code 0 75 | """) { 76 | style { 77 | backgroundColor += Color.BLACK 78 | textFill = Color.LIGHTGREY 79 | fontFamily = "Consolas" 80 | } 81 | } 82 | } 83 | 84 | item("Events") { 85 | 86 | } 87 | } 88 | with(leftDrawer) { 89 | item() 90 | item("Form item") { 91 | form { 92 | fieldset("Customer Details") { 93 | field("Name") { textfield() } 94 | field("Password") { textfield() } 95 | } 96 | } 97 | } 98 | item("SqueezeBox Item", showHeader = false) { 99 | squeezebox(multiselect = false, fillHeight = true) { 100 | fold("Customer Editor") { 101 | form { 102 | fieldset("Customer Details") { 103 | field("Name") { textfield() } 104 | field("Password") { textfield() } 105 | } 106 | } 107 | } 108 | fold("Some other editor") { 109 | stackpane { 110 | label("Nothing here") 111 | } 112 | } 113 | fold("A Table") { 114 | tableview(observableListOf("One", "Two", "Three")) { 115 | column("Value") { SimpleStringProperty(it.value) } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/EventBusTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleObjectProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | import tornadofx.* 6 | import tornadofx.EventBus.RunOn.BackgroundThread 7 | import java.time.LocalDateTime 8 | import kotlin.concurrent.timer 9 | 10 | class EventBusTestApp : App(EventBusTestView::class) { 11 | init { 12 | find() 13 | } 14 | } 15 | 16 | class MyDataEvent(val data: List) : FXEvent() 17 | object GiveMeData : FXEvent(BackgroundThread) 18 | 19 | data class MyEvent(val text: String) : FXEvent() 20 | 21 | class EventBusTestView : View("Data Event Table") { 22 | val labelText = SimpleStringProperty("") 23 | val regProperty = SimpleObjectProperty() 24 | var reg by regProperty 25 | 26 | 27 | override val root = borderpane { 28 | top { 29 | hbox(10) { 30 | button("Load data").action { 31 | fire(GiveMeData) 32 | } 33 | button("Subscribe label") { 34 | setOnAction { 35 | reg = subscribe { 36 | labelText.value = it.text 37 | } 38 | } 39 | enableWhen { regProperty.isNull } 40 | } 41 | button("Unsubscribe label") { 42 | setOnAction { 43 | reg!!.unsubscribe() 44 | reg = null 45 | } 46 | enableWhen { regProperty.isNotNull } 47 | } 48 | } 49 | } 50 | center { 51 | tableview { 52 | column("Value", String::class) { 53 | value { it.value } 54 | } 55 | subscribe { 56 | items.setAll(it.data) 57 | selectionModel.select(0) 58 | requestFocus() 59 | } 60 | } 61 | } 62 | bottom { 63 | label(labelText) 64 | } 65 | } 66 | 67 | init { 68 | timer(daemon = true, period = 1000) { 69 | fire(MyEvent(LocalDateTime.now().toString())) 70 | } 71 | } 72 | } 73 | 74 | class MyController : Controller() { 75 | init { 76 | subscribe { 77 | fire(MyDataEvent(listOf("Simulate", "Data", "Loaded", "From", "The", "Database"))) 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ExpandableTableTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.ObservableList 4 | import javafx.scene.layout.Background.EMPTY 5 | import tornadofx.* 6 | import java.time.LocalDate 7 | import kotlin.random.Random 8 | 9 | class ExpandableTableTest : View("Smart Resize Demo") { 10 | class Room(val id: Int, val number: String, val type: String, val bed: String, val occupancy: ObservableList) 11 | class Occupancy(val id: Int, val date: LocalDate, val customer: Int) 12 | 13 | val rooms = observableListOf( 14 | Room(1, "104", "Bedroom", "Queen", makeOccupancy(5)), 15 | Room(2, "105", "Bedroom", "King", makeOccupancy(5)), 16 | Room(3, "106", "Bedroom", "King", makeOccupancy(5)), 17 | Room(4, "107", "Suite", "Queen", makeOccupancy(5)), 18 | Room(4, "108", "Bedroom", "King", makeOccupancy(5)), 19 | Room(4, "109", "Conference Room", "Queen", makeOccupancy(5)), 20 | Room(4, "110", "Bedroom", "Queen", makeOccupancy(5)), 21 | Room(4, "111", "Playroom", "King", makeOccupancy(5)), 22 | Room(4, "112", "Bedroom", "Queen", makeOccupancy(5)) 23 | ) 24 | 25 | override val root = tableview(rooms) { 26 | prefWidth = 600.0 27 | 28 | readonlyColumn("#", Room::id).contentWidth(10.0, true, true) 29 | readonlyColumn("Number", Room::number).weightedWidth(1.0) 30 | readonlyColumn("Bed", Room::bed).apply { 31 | weightedWidth(2.0) 32 | cellFormat { 33 | graphic = cache { 34 | textfield(itemProperty()) { 35 | isEditable = false 36 | paddingAll = 0 37 | background = EMPTY 38 | } 39 | } 40 | } 41 | } 42 | readonlyColumn("Type", Room::type).weightedWidth(2.0) 43 | 44 | smartResize() 45 | 46 | rowExpander { 47 | tableview(it.occupancy) { 48 | readonlyColumn("Occupancy", Occupancy::id) 49 | readonlyColumn("Date", Occupancy::date) 50 | readonlyColumn("Customer", Occupancy::customer) 51 | prefHeight = 100.0 52 | } 53 | } 54 | } 55 | 56 | init { 57 | runAsync { 58 | Thread.sleep(5000) 59 | } ui { 60 | root.items.add(Room(4, "113", "Suite", "King size long description", makeOccupancy(5))) 61 | SmartResize.POLICY.requestResize(root) 62 | } 63 | } 64 | 65 | } 66 | 67 | 68 | private fun makeOccupancy(count: Int) = (0..count).map { 69 | ExpandableTableTest.Occupancy(Random.nextInt(100), LocalDate.now().minusDays(it.toLong()), Random.nextInt(100000)) 70 | }.asObservable() 71 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ExternalStylesheetTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.text.FontWeight 4 | import javafx.stage.Stage 5 | import tornadofx.* 6 | 7 | class ExternalStylesheetTestApp : App(ExternalStylesheetTestView::class) { 8 | override fun start(stage: Stage) { 9 | importStylesheet("https://raw.githubusercontent.com/edvin/tornadofx/master/src/test/resources/teststyles.css") 10 | super.start(stage) 11 | } 12 | } 13 | 14 | class ExternalStylesheetTestView: View("External Stylesheet") { 15 | override val root = label(title).addClass("testclass") 16 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/FormTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.geometry.Orientation 4 | import tornadofx.* 5 | 6 | class FormApp : App(FormView::class) 7 | 8 | class FormView : View("My Form") { 9 | override val root = form { 10 | fieldset(title) { 11 | field("Name") { 12 | textfield() 13 | } 14 | field("Date Of _Birth") { 15 | textfield { 16 | mnemonicTarget() 17 | } 18 | } 19 | field("Height") { 20 | textfield() 21 | } 22 | field("Weight") { 23 | textfield() 24 | } 25 | buttonbar { 26 | button("Save") 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/GotoViewApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class SwitchViewApp : App(ContainerView::class) 6 | 7 | class ContainerView : View("ContainerView") { 8 | val subView1: SubView1 by inject() 9 | val subView2: SubView2 by inject() 10 | 11 | override val root = borderpane { 12 | top { 13 | hbox(10) { 14 | button("Switch view").action { 15 | if (center.lookup("#view1") != null) 16 | subView1.replaceWith(subView2, ViewTransition.Slide(0.2.seconds)) 17 | else 18 | subView2.replaceWith(subView1, ViewTransition.Slide(0.2.seconds, ViewTransition.Direction.RIGHT)) 19 | } 20 | button("Fire event").action { fire(MySwitchViewEvent) } 21 | } 22 | } 23 | center { 24 | add(subView1) 25 | } 26 | } 27 | } 28 | 29 | class SubView1 : View("SubView1") { 30 | override val root = hbox { 31 | id = "view1" 32 | label("I'm subview 1") 33 | } 34 | init { 35 | subscribe { 36 | println("SubView1 received event") 37 | } 38 | } 39 | } 40 | 41 | class SubView2 : View("SubView2") { 42 | override val root = hbox { 43 | label("I'm subview 2") 44 | } 45 | init { 46 | subscribe { 47 | println("SubView2 received event") 48 | } 49 | } 50 | } 51 | 52 | object MySwitchViewEvent : FXEvent() -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ImportStyles.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.paint.Color 4 | import javafx.scene.text.FontWeight 5 | import tornadofx.* 6 | 7 | /** 8 | * @author I58695 (2016-08-18) 9 | */ 10 | class ImportStyles : App(MainView::class, Styles::class) { 11 | init { 12 | dumpStylesheets() 13 | } 14 | 15 | class MainView : View() { 16 | override val root = vbox { 17 | addClass(box) 18 | label("Test").addClass(test) 19 | } 20 | } 21 | 22 | companion object { 23 | val box by cssclass() 24 | val test by cssclass() 25 | } 26 | 27 | class Styles : Stylesheet(ParentStyles::class) { 28 | init { 29 | test { 30 | fontSize = 36.px 31 | fontWeight = FontWeight.BOLD 32 | textFill = Color.WHITE 33 | } 34 | } 35 | } 36 | 37 | class ParentStyles : Stylesheet() { 38 | init { 39 | box { 40 | backgroundColor += Color.GRAY 41 | padding = box(10.px, 100.px) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/InternalWindowTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class InternalWindowTestApp : App(InternalWindowTest::class) 6 | 7 | class InternalWindowTest : View("Internal Window") { 8 | override val root = stackpane { 9 | setPrefSize(600.0, 400.0) 10 | button("Open editor").action { 11 | openInternalWindow(modal = false, icon = FX.icon) 12 | } 13 | 14 | } 15 | } 16 | 17 | class Editor : View("Editor") { 18 | override val root = form { 19 | prefWidth = 300.0 20 | 21 | fieldset("Editor") { 22 | field("First field") { 23 | textfield() 24 | } 25 | field("Second field") { 26 | textfield() 27 | } 28 | button("Save") { 29 | shortcut("Alt+S") 30 | action { 31 | save() 32 | } 33 | } 34 | } 35 | } 36 | 37 | private fun save() { 38 | close() 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/KeyboardTestView.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class KeyboardTestApp : App(KeyboardTestView::class) 6 | 7 | class KeyboardTestView : View("TornadoFX Keyboard Layout") { 8 | override val root = vbox(30) { 9 | paddingAll = 40 10 | // Loaded with JSON 11 | keyboard { 12 | load(resources.json("/tornadofx/tests/TornadoKeyboard.json")) 13 | println(toKeyboardLayoutEditorFormat()) 14 | } 15 | // Generated with builders 16 | keyboard { 17 | row { 18 | key("", width = 1.25) 19 | key("") 20 | key("") 21 | key("") 22 | key("") 23 | key("") 24 | key("Del", height = 2) 25 | key("") 26 | key("") 27 | key("") 28 | key("") 29 | key("") 30 | key("") 31 | key("") 32 | } 33 | row { 34 | key("", width = 1.25) 35 | key("") 36 | key("Ctrl+W") 37 | key("Esc") 38 | key("") 39 | key("") 40 | spacer() 41 | key("") 42 | key("PgUp") 43 | key("▲") 44 | key("PgDn") 45 | key("") 46 | key("") 47 | key("") 48 | } 49 | row { 50 | key("", width = 1.25) 51 | key("") 52 | key("") 53 | key("") 54 | key("") 55 | key("") 56 | key("", height = 2) 57 | key("Home") 58 | key("◀") 59 | key("▼") 60 | key("▶") 61 | key("Ins") 62 | key("Del") 63 | key("") 64 | } 65 | row { 66 | key("", width = 1.25) 67 | key("Ctrl+Z") 68 | key("Ctrl+X") 69 | key("Ctrl+C") 70 | key("Ctrl+V") 71 | key("") 72 | spacer() 73 | key("End") 74 | key("") 75 | key("") 76 | key("") 77 | key("") 78 | key("", width = 2) 79 | } 80 | row { 81 | key("", width = 1.25) 82 | key("", width = 1.25) 83 | key("", width = 1.25) 84 | key("FN (Pressed)", width = 2.75) 85 | key("", width = 2.75) 86 | key("", width = 1.25) 87 | key("", width = 1.25) 88 | key("", width = 1.25) 89 | key("", width = 1.25) 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ListMenuTest.kt: -------------------------------------------------------------------------------- 1 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon 2 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.* 3 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView 4 | import javafx.geometry.Orientation 5 | import javafx.geometry.Pos 6 | import javafx.geometry.Side 7 | import javafx.scene.paint.Color 8 | import tornadofx.* 9 | 10 | class ListMenuTestApp : App(ListMenuTest::class) 11 | 12 | class ListMenuTest : View("ListMenu Test") { 13 | val listmenu = listmenu(theme = "blue") { 14 | addMenuItems() 15 | activeItem = items.first() 16 | maxHeight = Double.MAX_VALUE 17 | } 18 | 19 | override val root = borderpane { 20 | setPrefSize(650.0, 500.0) 21 | top { 22 | vbox(10) { 23 | label(title).style { fontSize = 3.em } 24 | hbox(10) { 25 | alignment = Pos.CENTER 26 | label("Orientation") 27 | combobox(listmenu.orientationProperty, Orientation.values().toList()) 28 | label("Icon Position") 29 | combobox(listmenu.iconPositionProperty, values = Side.values().toList()) 30 | label("Theme") 31 | combobox(listmenu.themeProperty, values = listOf("none", "blue")) 32 | checkbox("Icons Only") { 33 | selectedProperty().onChange { 34 | with(listmenu) { 35 | items.clear() 36 | if (it) { 37 | iconOnlyMenuItems() 38 | } else { 39 | addMenuItems() 40 | } 41 | } 42 | } 43 | } 44 | } 45 | label(stringBinding(listmenu.activeItemProperty) { "Currently selected: ${value?.text}" }) { 46 | style { textFill = Color.RED } 47 | } 48 | style { 49 | alignment = Pos.CENTER 50 | } 51 | } 52 | } 53 | center = listmenu 54 | style { 55 | backgroundColor += Color.WHITE 56 | } 57 | paddingAll = 20 58 | } 59 | 60 | private fun ListMenu.iconOnlyMenuItems() { 61 | item(graphic = icon(USER)) 62 | item(graphic = icon(SUITCASE)) 63 | item(graphic = icon(COG)) 64 | } 65 | 66 | private fun ListMenu.addMenuItems() { 67 | item("Contacts", icon(USER)) 68 | item("Projects", icon(SUITCASE)) 69 | item("Settings", icon(COG)) 70 | } 71 | 72 | private fun icon(icon: FontAwesomeIcon) = FontAwesomeIconView(icon).apply { glyphSize = 20 } 73 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/MultipleLifecycleAsyncApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import tornadofx.* 5 | 6 | class MultipleLifecycleAsyncView : View("Multiple Lifecycle Async") { 7 | val counterProperty = SimpleIntegerProperty() 8 | var counter by counterProperty 9 | override val root = pane { 10 | button("Increment on background thread and main thread") { 11 | action { 12 | runAsync { 13 | counter++ 14 | } success { 15 | counter++ 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/NestedTableColumns.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.FXCollections 4 | import tornadofx.* 5 | 6 | class NestedTableColumnsApp : App(NestedTableColumns::class) 7 | 8 | class NestedTableColumns : View("Nested Table Columns") { 9 | data class Person(val name: String, val primaryEmail: String, val secondaryEmail: String) 10 | 11 | val people = FXCollections.observableArrayList( 12 | Person("John Doe", "john@doe.com", "john.doe@gmail.com"), 13 | Person("Jane Doe", "jane@doe.com", "jane.doe@gmail.com") 14 | ) 15 | 16 | override val root = tableview(people) { 17 | readonlyColumn("Name", Person::name) 18 | nestedColumn("Email addresses") { 19 | readonlyColumn("Primary Email", Person::primaryEmail) 20 | readonlyColumn("Secondary Email", Person::secondaryEmail) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/PaddingPropertyTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.paint.Color 4 | import tornadofx.* 5 | 6 | class PaddingPropertyTestApp : App(PaddingPropertyTest::class) 7 | 8 | class PaddingPropertyTest : View("Slide Padding") { 9 | override val root = vbox { 10 | setPrefSize(400.0, 150.0) 11 | style { backgroundColor += Color.CADETBLUE } 12 | slider(0.0, 100.0, 20.0) { 13 | this@vbox.paddingAllProperty.bind(valueProperty()) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/PaintBackgroundTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.paint.Color 4 | import tornadofx.* 5 | 6 | class PaintBackgroundTestApp : App(PaintBackgroundTest::class) 7 | 8 | class PaintBackgroundTest : View() { 9 | override val root = hbox { 10 | label("Hello Background Color") { 11 | background = Color.LIGHTBLUE.asBackground() 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ParamTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | import java.time.LocalDateTime 5 | 6 | class ParamTestApp : App(ParamCallerView::class) 7 | 8 | class ParamCallerView : View() { 9 | override val root = vbox { 10 | button("Open modal with paramMap"){ 11 | action { 12 | find(mapOf("name" to "Param ${LocalDateTime.now()}")).openModal() 13 | } 14 | } 15 | button("Open modal with paramVararg"){ 16 | action { 17 | find("name" to "Param ${LocalDateTime.now()}").openModal() 18 | } 19 | } 20 | } 21 | } 22 | 23 | class ParamReceiverView : View() { 24 | val name: String by param() 25 | 26 | override val root = vbox() 27 | 28 | override fun onDock() { 29 | with(root) { 30 | label(name) 31 | } 32 | currentStage?.sizeToScene() 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/PojoTableColumns.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.FXCollections 4 | import tornadofx.* 5 | import tornadofx.tests.JavaPerson 6 | 7 | class PojoTableColumnsApp : App(PojoTableColumns::class) 8 | 9 | class PojoTableColumns : View("Pojo Table Columns") { 10 | val people = FXCollections.observableArrayList( 11 | JavaPerson().apply { name = "John Doe"; primaryEmail = "john@doe.com"; secondaryEmail = "john.doe@gmail.com"}, 12 | JavaPerson().apply { name = "Jane Doe"; primaryEmail = "jane@doe.com"; secondaryEmail = "jane.doe@gmail.com"} 13 | ) 14 | 15 | override val root = tableview(people) { 16 | column("Name", JavaPerson::getName ) 17 | nestedColumn("Email addresses") { 18 | column("Primary Email", JavaPerson::getPrimaryEmail ) 19 | column("Secondary Email", JavaPerson::getSecondaryEmail ) 20 | } 21 | resizeColumnsToFitContent() 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/PojoTreeTableColumns.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.control.TreeItem 4 | import tornadofx.* 5 | import tornadofx.tests.JavaPerson 6 | 7 | class PojoTreeTableColumnsApp : App(PojoTreeTableColumns::class) 8 | 9 | class PojoTreeTableColumns : View("Pojo Tree Table Columns") { 10 | 11 | val people = observableListOf( 12 | JavaPerson("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", "mary2.hanes@contoso.com", listOf( 13 | JavaPerson("Jacob Mays", "IT Help Desk", "jacob.mays@contoso.com", "jacob2.mays@contoso.com"), 14 | JavaPerson("John Ramsy", "IT Help Desk", "john.ramsy@contoso.com", "john2.ramsy@contoso.com"))), 15 | JavaPerson("Erin James", "Human Resources", "erin.james@contoso.com", "erin2.james@contoso.com", listOf( 16 | JavaPerson("Erlick Foyes", "Customer Service", "erlick.foyes@contoso.com", "erlick2.foyes@contoso.com"), 17 | JavaPerson("Steve Folley", "Customer Service", "steve.folley@contoso.com", "erlick2.foyes@contoso.com"), 18 | JavaPerson("Larry Cable", "Customer Service", "larry.cable@contoso.com", "larry2.cable@contoso.com"))) 19 | ) 20 | 21 | // Create the root item that holds all top level employees 22 | val rootItem = TreeItem(JavaPerson().apply { name = "Employees by Manager"; employees = people }) 23 | 24 | override val root = treetableview(rootItem) { 25 | prefWidth = 800.0 26 | 27 | column("Name", "name").contentWidth(50) // non type safe 28 | column("Department", JavaPerson::getDepartment).remainingWidth() 29 | nestedColumn("Email addresses") { 30 | column("Primary Email", JavaPerson::getPrimaryEmail) 31 | column("Secondary Email", JavaPerson::getSecondaryEmail) 32 | } 33 | 34 | // Always return employees under the current person 35 | populate { it.value.employees } 36 | 37 | // Expand the two first levels 38 | root.isExpanded = true 39 | root.children.withEach { isExpanded = true } 40 | smartResize() 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ReloadStylesInModal.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class ReloadStylesInModal : App(MainView::class, Styles::class) { 6 | class MainView : View() { 7 | override val root = hbox { 8 | button("Open").action { 9 | find().openModal() 10 | } 11 | } 12 | } 13 | 14 | class MyModal : Fragment() { 15 | override val root = vbox { 16 | label("My label") 17 | button("Close").action { 18 | close() 19 | } 20 | } 21 | } 22 | 23 | class Styles : Stylesheet(Base::class) 24 | 25 | class Base : Stylesheet() { 26 | init { 27 | button { 28 | fontSize = 30.px 29 | } 30 | label { 31 | fontSize = 30.px 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/RemoveTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import javafx.scene.paint.Color 5 | import javafx.scene.text.FontWeight 6 | import tornadofx.* 7 | 8 | class RemoveTestApp : App(RemoveTest::class) 9 | 10 | class RemoveTest : View("Remove Test") { 11 | val remove = SimpleBooleanProperty(false) 12 | 13 | override val root = vbox { 14 | setPrefSize(400.0, 200.0) 15 | form { 16 | label("I'm above the removable element (which might be removed of course)") 17 | label("Remove me!") { 18 | removeWhen { remove } 19 | style { 20 | fontWeight = FontWeight.BOLD 21 | textFill = Color.RED 22 | } 23 | } 24 | label("I'm below the removable element (which might be removed of course)") 25 | fieldset { 26 | field("Remove") { 27 | combobox(remove, values = listOf(true, false)) 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ShortLongPressTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.FXCollections 4 | import tornadofx.* 5 | import java.time.LocalDateTime 6 | 7 | class ShortLongPressTestApp : App(ShortLongPressTest::class) 8 | 9 | class ShortLongPressTest : View("Short or long press") { 10 | val actuations = FXCollections.observableArrayList() 11 | 12 | override val root = borderpane { 13 | top { 14 | stackpane { 15 | paddingAll = 20 16 | button("Short or longpress me") { 17 | longpress { actuations.add("Long at ${LocalDateTime.now()}") } 18 | shortpress { actuations.add("Short at ${LocalDateTime.now()}") } 19 | } 20 | } 21 | } 22 | center { 23 | listview(actuations) 24 | } 25 | bottom { 26 | label("Hold for 700ms to actuate long action, or less to actuate short action") { 27 | paddingAll = 20 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/SlideshowTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class SlideshowTestApp : App(SlideshowTest::class, NewViewTransitionStyles::class) 6 | 7 | class SlideshowTest : View("Slideshow") { 8 | override val root = slideshow { 9 | slide() 10 | slide(ViewTransition.Fade(0.3.seconds)) 11 | slide() 12 | } 13 | } 14 | 15 | class Slide1 : View("Slide 1") { 16 | override val root = stackpane { 17 | label(titleProperty) 18 | addClass(NewViewTransitionStyles.box, NewViewTransitionStyles.blue) 19 | } 20 | } 21 | 22 | class Slide2 : View("Slide 2") { 23 | override val root = stackpane { 24 | label(titleProperty) 25 | addClass(NewViewTransitionStyles.box, NewViewTransitionStyles.red) 26 | } 27 | } 28 | 29 | class Slide3 : View("Slide 3") { 30 | override val root = stackpane { 31 | label(titleProperty) 32 | addClass(NewViewTransitionStyles.box, NewViewTransitionStyles.blue) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/SqueezeBoxTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.geometry.Pos 4 | import javafx.scene.input.KeyCombination 5 | import tornadofx.* 6 | 7 | class SqueezeBoxTestApp : App(SqueezeBoxTestView::class) 8 | 9 | object AddFoldEvent : FXEvent() 10 | 11 | class SqueezeBoxTestView : View("SqueezeBox Multiple Open Folds") { 12 | override val root = vbox(5.0) { 13 | setPrefSize(300.0, 600.0) 14 | 15 | button("Add node") { 16 | action { 17 | fire(AddFoldEvent) 18 | } 19 | shortcut(KeyCombination.valueOf("Ctrl+A")) 20 | } 21 | 22 | scrollpane(fitToWidth = true) { 23 | squeezebox { 24 | fold("Pane 1", expanded = true, closeable = true) { 25 | stackpane { 26 | vbox { 27 | alignment = Pos.CENTER 28 | label("I'm inside 1") 29 | label("Me too!") 30 | } 31 | } 32 | } 33 | fold("Pane 2", expanded = true, closeable = true) { 34 | label("I'm inside 2") 35 | } 36 | fold("Pane 3", closeable = true) { 37 | label("I'm inside 3") 38 | } 39 | fold("Pane 4", expanded = true, closeable = true) { 40 | label("I'm inside 4") 41 | } 42 | fold("Pane 5", closeable = true) { 43 | label("I'm inside 5") 44 | } 45 | subscribe { 46 | fold("Another fold by subscription", closeable = true) { 47 | stackpane { 48 | label("Yo!") 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/StylesheetErrorTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.App 4 | import tornadofx.Stylesheet 5 | import tornadofx.View 6 | import tornadofx.addClass 7 | import tornadofx.button 8 | import tornadofx.stackpane 9 | 10 | class StylesheetErrorTest : App(StylesheetErrorView::class, Styles::class) 11 | 12 | class StylesheetErrorView : View() { 13 | override val root = stackpane { 14 | button("Click here").addClass("my-button") 15 | } 16 | } 17 | 18 | class Styles : Stylesheet() { 19 | init { 20 | text { 21 | // cannot refer to unset property 22 | stroke = baseColor 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/SwitchViewApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.control.Alert.AlertType.ERROR 4 | import tornadofx.* 5 | 6 | class GotoViewApp : App(GotoContainerView::class) 7 | 8 | class GotoContainerView : View("ContainerView") { 9 | val gotoSubView1: GotoSubView1 by inject() 10 | val gotoSubView2: GotoSubView2 by inject() 11 | 12 | override val root = borderpane { 13 | top { 14 | button("Goto view").action { 15 | if (center.lookup("#view1") != null) 16 | gotoSubView1.goto(gotoSubView2) 17 | else 18 | gotoSubView2.goto(gotoSubView1) 19 | } 20 | } 21 | center { 22 | add(gotoSubView1) 23 | } 24 | } 25 | } 26 | 27 | class GotoSubView1 : View("GotoSubView2") { 28 | override val root = hbox { 29 | id = "view1" 30 | label("I'm subview 1") 31 | } 32 | 33 | override fun onGoto(source: UIComponent) { 34 | when (source) { 35 | is GotoSubView2 -> if (source.isDocked) source.replaceWith(this) else openModal() 36 | is GotoSubView1 -> source.replaceWith(this) 37 | else -> alert(ERROR, "I refuse!", "Not going to happen") 38 | } 39 | } 40 | } 41 | 42 | class GotoSubView2 : View("GotoSubView2") { 43 | override val root = hbox { 44 | label("I'm subview 2") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TabPaneTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import tornadofx.* 5 | 6 | class TabPaneTestApp : App(TabPaneTest::class) 7 | 8 | class TabPaneTest : View("TabPane Test") { 9 | override val root = tabpane { 10 | tab() 11 | tab() 12 | // This is not the typical use case for using index, but instead when tab is added programmatically, 13 | // ex.: duplicate the current tab 14 | tab(1, "Tab Three", TabThree().root) 15 | // Here we verify that the original implementation works correctly, i.e. adds the tab to the end of the list 16 | tab("Tab Four", TabFour().root) 17 | } 18 | } 19 | 20 | /** 21 | * Check that the tab title updates when the input changes. 22 | * The Tab should not be closeable as it binds to the closeable property. 23 | */ 24 | class TabOne : View("Tab One") { 25 | override val root = textfield(titleProperty) 26 | override val closeable = SimpleBooleanProperty(false) 27 | } 28 | 29 | class TabTwo : View("Tab Two") { 30 | override val root = textfield(titleProperty) 31 | override val closeable = SimpleBooleanProperty(false) 32 | } 33 | 34 | class TabThree : View("Tab Three") { 35 | override val root = textfield(titleProperty) 36 | override val closeable = SimpleBooleanProperty(false) 37 | } 38 | 39 | class TabFour : View("Tab Four") { 40 | override val root = textfield(titleProperty) 41 | override val closeable = SimpleBooleanProperty(false) 42 | } 43 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewCellCacheTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | 5 | class TableViewCellCacheTestApp : App(TableViewCellCacheTest::class) 6 | 7 | class TableViewCellCacheTest : View("TableView CellCache Test") { 8 | val kittens = DataGridTestApp.images["kittens"] as List 9 | 10 | override val root = tableview(kittens.asObservable()) { 11 | prefWidth = 300.0 12 | 13 | column("Filename", String::class) { 14 | value { it.value.substringAfterLast("/") } 15 | } 16 | column("Image", String::class) { 17 | value { it.value } 18 | cellCache { imageview(it, true) } 19 | cellFormat { 20 | prefHeight = 160.0 21 | prefWidth = 160.0 22 | } 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewCheckboxReflection.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import tornadofx.* 5 | 6 | class TableViewCheckboxApp : App(TableViewCheckbox::class) 7 | 8 | class Model { 9 | val booleanProperty = SimpleBooleanProperty() 10 | } 11 | 12 | class TableViewCheckbox : View() { 13 | val items = observableListOf(Model()) 14 | 15 | override val root = tableview(items) { 16 | column("Checkbox", Model::booleanProperty) 17 | .useCheckbox() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewDNDTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.control.TableRow 4 | import javafx.scene.input.ClipboardContent 5 | import javafx.scene.input.TransferMode 6 | import javafx.scene.paint.Color 7 | import javafx.scene.text.FontWeight 8 | import tornadofx.* 9 | import tornadofx.tests.Person 10 | import tornadofx.tests.PersonRef 11 | 12 | class DNDReorderTableViewApp : App(DNDReorderTableView::class) 13 | 14 | class DNDReorderTableView : View() { 15 | val people = observableListOf(Person("Bob", 42), Person("Jane", 24), Person("John", 31)) 16 | 17 | override val root = tableview(people) { 18 | column("Name", Person::nameProperty) 19 | column("Age", Person::ageProperty) 20 | 21 | setOnDragDetected { event -> 22 | val person = if (!selectionModel.isEmpty) selectedItem else null 23 | if (person != null) { 24 | val dragboard = startDragAndDrop(TransferMode.MOVE) 25 | dragboard.setContent(ClipboardContent().apply { put(Person.DATA_FORMAT, PersonRef(person.id)) }) 26 | } 27 | event.consume() 28 | } 29 | 30 | // Handle DND per row 31 | setRowFactory { 32 | object : TableRow() { 33 | init { 34 | setOnDragOver { 35 | if (it.dragboard.hasContent(Person.DATA_FORMAT)) { 36 | it.acceptTransferModes(TransferMode.MOVE) 37 | it.consume() 38 | } 39 | } 40 | setOnDragEntered { 41 | if (it.dragboard.hasContent(Person.DATA_FORMAT)) { 42 | val ref = it.dragboard.getContent(Person.DATA_FORMAT) as PersonRef 43 | style { 44 | if (ref.id != item.id) { 45 | fontWeight = FontWeight.BOLD 46 | } else { 47 | textFill = Color.RED 48 | } 49 | } 50 | it.consume() 51 | } 52 | } 53 | setOnDragExited { 54 | style = null 55 | it.consume() 56 | } 57 | setOnDragDropped { 58 | if (it.dragboard.hasContent(Person.DATA_FORMAT)) { 59 | val draggedRef = it.dragboard.getContent(Person.DATA_FORMAT) as PersonRef 60 | val droppedPerson = item 61 | val targetIndex = items.indexOf(droppedPerson) 62 | val sourcePerson = items.find { it.id == draggedRef.id } 63 | items.move(sourcePerson, targetIndex) 64 | it.isDropCompleted = true 65 | } else { 66 | it.isDropCompleted = false 67 | } 68 | it.consume() 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewDirtyTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleObjectProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | import javafx.collections.FXCollections 6 | import javafx.scene.control.SelectionMode 7 | import javafx.scene.control.TableView 8 | import javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY 9 | import javafx.scene.text.FontWeight 10 | import tornadofx.* 11 | import java.util.* 12 | 13 | class TableViewDirtyTestApp : WorkspaceApp(TableViewDirtyTest::class) 14 | 15 | class TableViewDirtyTest : View("Dirty Tables") { 16 | val customers = FXCollections.observableArrayList(Customer("Thomas", "Nield"), Customer("Matthew", "Turnblom"), Customer("Edvin", "Syse")) 17 | var table: TableView by singleAssign() 18 | 19 | override val root = borderpane { 20 | center { 21 | tableview(customers) { 22 | table = this 23 | prefHeight = 200.0 24 | 25 | column("First Name", Customer::firstNameProperty) { 26 | makeEditable() 27 | cellDecorator { 28 | style { 29 | fontWeight = FontWeight.BOLD 30 | } 31 | } 32 | } 33 | column("Last Name", Customer::lastNameProperty).makeEditable() 34 | enableCellEditing() 35 | regainFocusAfterEdit() 36 | enableDirtyTracking() 37 | selectOnDrag() 38 | contextmenu { 39 | item(stringBinding(selectionModel.selectedCells) { "Rollback ${selectedColumn?.text}" }) { 40 | disableWhen { editModel.selectedItemDirty.not() } 41 | action { editModel.rollback(selectedItem, selectedColumn) } 42 | } 43 | item(stringBinding(selectionModel.selectedCells) { "Commit ${selectedColumn?.text}" }) { 44 | disableWhen { editModel.selectedItemDirty.not() } 45 | action { editModel.commit(selectedItem, selectedColumn) } 46 | } 47 | } 48 | selectionModel.selectionMode = SelectionMode.MULTIPLE 49 | columnResizePolicy = CONSTRAINED_RESIZE_POLICY 50 | } 51 | } 52 | bottom { 53 | hbox(10) { 54 | paddingAll = 4 55 | button("Commit row") { 56 | disableWhen { table.editModel.selectedItemDirty.not() } 57 | action { table.editModel.commitSelected() } 58 | } 59 | button("Rollback row") { 60 | disableWhen { table.editModel.selectedItemDirty.not() } 61 | action { table.editModel.rollbackSelected() } 62 | } 63 | } 64 | } 65 | } 66 | 67 | override fun onSave() { 68 | table.editModel.commit() 69 | } 70 | 71 | override fun onRefresh() { 72 | table.editModel.rollback() 73 | } 74 | 75 | init { 76 | icon = FX.icon 77 | } 78 | 79 | class Customer(firstName: String, lastName: String) { 80 | val idProperty = SimpleObjectProperty(UUID.randomUUID()) 81 | var id by idProperty 82 | 83 | val firstNameProperty = SimpleStringProperty(firstName) 84 | var firstName by firstNameProperty 85 | 86 | val lastNameProperty = SimpleStringProperty(lastName) 87 | val lastName by lastNameProperty 88 | 89 | override fun toString() = "$firstName $lastName" 90 | 91 | override fun equals(other: Any?): Boolean { 92 | if (this === other) return true 93 | if (other?.javaClass != javaClass) return false 94 | 95 | other as Customer 96 | 97 | if (id != other.id) return false 98 | 99 | return true 100 | } 101 | 102 | override fun hashCode(): Int { 103 | return id.hashCode() 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewSelectionTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.scene.control.TabPane 4 | import javafx.scene.control.TreeItem 5 | import tornadofx.* 6 | import tornadofx.tests.JavaPerson 7 | 8 | class TableViewSelectionTestApp : App(TableViewSelectionTest::class) 9 | 10 | class TableViewSelectionTest : View("Table Selection Test") { 11 | val people = observableListOf( 12 | JavaPerson("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", "mary2.hanes@contoso.com"), 13 | JavaPerson("Erin James", "Human Resources", "erin.james@contoso.com", "erin2.james@contoso.com") 14 | ) 15 | 16 | override val root = tabpane { 17 | prefWidth = 800.0 18 | tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE 19 | 20 | tab("Table") { 21 | tableview(people) { 22 | column("Name", JavaPerson::getName).contentWidth(50.0) 23 | column("Department", JavaPerson::getDepartment).remainingWidth() 24 | nestedColumn("Email addresses") { 25 | column("Primary Email", JavaPerson::getPrimaryEmail) 26 | column("Secondary Email", JavaPerson::getSecondaryEmail) 27 | } 28 | 29 | selectionModel.isCellSelectionEnabled = true 30 | smartResize() 31 | 32 | contextmenu { 33 | item("Print selected").action { println(selectedValue.toString()) } 34 | item("Print selected position").action { println("(${selectedCell!!.column}, ${selectedCell!!.row})") } 35 | item("Print selected column").action { println(selectedColumn!!.text) } 36 | separator() 37 | item("Print employee's name").action { println(selectedItem!!.name) } 38 | item("Print employee's department").action { println(selectedItem!!.department) } 39 | item("Print employee's primary email").action { println(selectedItem!!.primaryEmail) } 40 | item("Print employee's secondary email").action { println(selectedItem!!.secondaryEmail) } 41 | } 42 | } 43 | } 44 | 45 | tab("Tree Table") { 46 | val rootItem = TreeItem(JavaPerson().apply { name = "Employees"; employees = people }) 47 | 48 | treetableview(rootItem) { 49 | column("Name", JavaPerson::getName).contentWidth(50.0) 50 | column("Department", JavaPerson::getDepartment).remainingWidth() 51 | nestedColumn("Email addresses") { 52 | column("Primary Email", JavaPerson::getPrimaryEmail) 53 | column("Secondary Email", JavaPerson::getSecondaryEmail) 54 | } 55 | 56 | populate { it.value.employees } 57 | 58 | selectionModel.isCellSelectionEnabled = true 59 | root.isExpanded = true 60 | root.children.withEach { isExpanded = true } 61 | smartResize() 62 | 63 | contextmenu { 64 | item("Print selected").action { println(selectedValue.toString()) } 65 | item("Print selected position").action { println("(${selectedCell!!.column}, ${selectedCell!!.row})") } 66 | item("Print selected column").action { println(selectedColumn!!.text) } 67 | separator() 68 | item("Print employee's name").action { println(selectedItem!!.name) } 69 | item("Print employee's department").action { println(selectedItem!!.department) } 70 | item("Print employee's primary email").action { println(selectedItem!!.primaryEmail) } 71 | item("Print employee's secondary email").action { println(selectedItem!!.secondaryEmail) } 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TableViewSortFilterTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import tornadofx.* 4 | import java.time.LocalDate 5 | 6 | 7 | class TableViewSortFilterTestApp : App(TableViewSortFilterTest::class) 8 | 9 | class TableViewSortFilterTest : View("Table Sort and Filter") { 10 | data class Person(val id: Int, var name: String, var birthday: LocalDate) { 11 | val age: Int 12 | get() = birthday.until(LocalDate.now()).years 13 | } 14 | 15 | private val persons = listOf( 16 | Person(1, "Samantha Stuart", LocalDate.of(1981, 12, 4)), 17 | Person(2, "Tom Marks", LocalDate.of(2001, 1, 23)), 18 | Person(3, "Stuart Gills", LocalDate.of(1989, 5, 23)), 19 | Person(3, "Nicole Williams", LocalDate.of(1998, 8, 11)) 20 | ) 21 | 22 | val data = SortedFilteredList() 23 | 24 | override val root = vbox { 25 | textfield { 26 | data.filterWhen(textProperty(), { query, item -> item.name.contains(query, true) }) 27 | } 28 | 29 | tableview(data) { 30 | readonlyColumn("ID", Person::id) 31 | column("Name", Person::name) 32 | nestedColumn("DOB") { 33 | readonlyColumn("Birthday", Person::birthday) 34 | readonlyColumn("Age", Person::age).contentWidth() 35 | } 36 | smartResize() 37 | } 38 | 39 | hbox(5) { 40 | label("Filter count:") 41 | label(data.sizeProperty.stringBinding { "$it records matching filter"}) 42 | } 43 | } 44 | 45 | init { 46 | // Simulate data access 47 | data.asyncItems { persons } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TaskStatusTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleStringProperty 4 | import tornadofx.* 5 | 6 | class TaskStatusTest : View("Task Status Test") { 7 | val status = TaskStatus() 8 | val output = SimpleStringProperty("") 9 | 10 | override val root = borderpane { 11 | top { 12 | toolbar { 13 | button("Run Async").action { 14 | output.value += "\n" 15 | runAsync(status) { 16 | Thread.sleep(1000) 17 | "hello" 18 | } ui { 19 | output.value += "$it\n" 20 | } 21 | } 22 | button("Run Await").action { 23 | output.value += "\n" 24 | runAsync(status) { 25 | Thread.sleep(1000) 26 | "hello 2" 27 | } ui { 28 | output.value += "$it\n" 29 | } 30 | status.completed.awaitUntil() 31 | output.value += "Await exited\n" 32 | } 33 | enableWhen { status.running.not() } 34 | } 35 | } 36 | center { 37 | textarea(output) 38 | } 39 | bottom { 40 | label("Running...") { 41 | visibleWhen { status.running } 42 | } 43 | } 44 | } 45 | 46 | override fun onDock() { 47 | status.completed.onChange { 48 | output.value += "completed $it\n" 49 | } 50 | status.running.onChange { 51 | output.value += "running $it\n" 52 | } 53 | } 54 | } 55 | 56 | class TaskStatusApp : App(TaskStatusTest::class) 57 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TodoTestApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleBooleanProperty 4 | import javafx.beans.property.SimpleObjectProperty 5 | import javafx.beans.property.SimpleStringProperty 6 | import javafx.collections.FXCollections 7 | import javafx.geometry.Pos 8 | import javafx.scene.control.CheckBox 9 | import javafx.scene.control.RadioButton 10 | import javafx.scene.control.TableColumn 11 | import javafx.scene.control.ToggleGroup 12 | import tornadofx.* 13 | 14 | class TodoItem(text: String? = null, completed: Boolean = true) { 15 | val textProperty = SimpleStringProperty(text) 16 | var text by textProperty 17 | 18 | val completedProperty = SimpleBooleanProperty(completed) 19 | var completed by completedProperty 20 | 21 | override fun toString() = "[${if (completed) "X" else " "}] $text" 22 | } 23 | 24 | enum class TodoFilter { All, Completed, Active } 25 | 26 | class TodoList : View("Todo List") { 27 | val todos = SortedFilteredList(FXCollections.observableArrayList(TodoItem("Item 1"), TodoItem("Item 2"), TodoItem("Item 3", false), TodoItem("Item 4"), TodoItem("Item 5"))) 28 | val todoFilter = SimpleObjectProperty(TodoFilter.All) 29 | 30 | override val root = borderpane { 31 | center { 32 | tableview(todos) { 33 | setPrefSize(300.0, 200.0) 34 | column("Completed", TodoItem::completedProperty) { 35 | sortType = TableColumn.SortType.DESCENDING 36 | sortOrder.add(this) 37 | cellFormat { 38 | graphic = cache { 39 | alignment = Pos.CENTER 40 | checkbox { 41 | selectedProperty().bindBidirectional(itemProperty()) 42 | 43 | action { 44 | tableView.edit(index, tableColumn) 45 | commitEdit(!isSelected) 46 | sort() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | column("Text", TodoItem::textProperty).makeEditable() 53 | 54 | } 55 | } 56 | bottom { 57 | hbox { 58 | togglegroup { 59 | TodoFilter.values().forEach { 60 | radiobutton(value = it) 61 | } 62 | bind(todoFilter) 63 | } 64 | } 65 | todoFilter.onChange { 66 | todos.refilter() 67 | } 68 | } 69 | } 70 | 71 | init { 72 | todos.predicate = { 73 | when (todoFilter.value) { 74 | TodoFilter.All -> true 75 | TodoFilter.Completed -> it.completed 76 | TodoFilter.Active -> !it.completed 77 | } 78 | } 79 | } 80 | } 81 | 82 | class TodoTestApp : WorkspaceApp(TodoList::class) 83 | 84 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ToggleButtonTestApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleStringProperty 4 | import javafx.geometry.Insets 5 | import tornadofx.* 6 | 7 | /** 8 | * Test case for #785 9 | * 10 | * Functions to bind both String and ObservableValue 11 | * 12 | * @author carl 13 | */ 14 | 15 | class ToggleButtonTestView : View("Toggle Button Test") { 16 | override val root = vbox { 17 | togglebutton("String") 18 | togglebutton(SimpleStringProperty("Observable")) 19 | padding = Insets(10.0) 20 | spacing = 4.0 21 | } 22 | } 23 | 24 | class ToggleButtonTestApp : App(ToggleButtonTestView::class) 25 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/TransitionsTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.animation.Interpolator 4 | import javafx.scene.paint.Color 5 | import javafx.scene.shape.Rectangle 6 | import tornadofx.* 7 | 8 | class TransitionsTestApp : App(TransitionsTestView::class) 9 | 10 | class TransitionsTestView : View() { 11 | val r1 = Rectangle(20.0, 20.0, Color.RED) 12 | val r2 = Rectangle(20.0, 20.0, Color.YELLOW) 13 | val r3 = Rectangle(20.0, 20.0, Color.GREEN) 14 | val r4 = Rectangle(20.0, 20.0, Color.BLUE) 15 | 16 | override val root = vbox { 17 | button("Animate").action { 18 | sequentialTransition { 19 | timeline { 20 | keyframe(0.5.seconds) { 21 | keyvalue(r1.translateXProperty(), 50.0, interpolator = Interpolator.EASE_BOTH) 22 | } 23 | } 24 | timeline { 25 | keyframe(0.5.seconds) { 26 | keyvalue(r2.translateXProperty(), 100.0, interpolator = Interpolator.EASE_BOTH) 27 | } 28 | } 29 | timeline { 30 | keyframe(0.5.seconds) { 31 | keyvalue(r3.translateXProperty(), 150.0, interpolator = Interpolator.EASE_BOTH) 32 | } 33 | } 34 | timeline { 35 | keyframe(0.5.seconds) { 36 | keyvalue(r4.translateXProperty(), 200.0, interpolator = Interpolator.EASE_BOTH) 37 | } 38 | } 39 | } 40 | } 41 | pane { 42 | add(r1) 43 | add(r2) 44 | add(r3) 45 | add(r4) 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/ViewDockingApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import javafx.scene.Scene 5 | import javafx.stage.Stage 6 | import tornadofx.* 7 | import java.util.concurrent.atomic.AtomicInteger 8 | 9 | class ViewDockingApp : App(PrimaryDockingView::class) 10 | 11 | open class PrimaryDockingView : View() { 12 | 13 | companion object { 14 | val instanceCounter = AtomicInteger() 15 | } 16 | val instanceId = instanceCounter.getAndIncrement() 17 | val dockCounterProperty = SimpleIntegerProperty() 18 | var dockCounter by dockCounterProperty 19 | val undockCounterProperty = SimpleIntegerProperty() 20 | var undockCounter by undockCounterProperty 21 | 22 | init { 23 | println("$tag.init()") 24 | } 25 | 26 | open val tag: String 27 | get() = "PrimaryDockingView" 28 | 29 | override val root = vbox { 30 | form { 31 | fieldset { 32 | field("Instance ID") { 33 | label(instanceId.toString()) 34 | } 35 | field("Dock Counter") { 36 | label(dockCounterProperty) { 37 | style { 38 | fontSize = 48.pt 39 | } 40 | } 41 | } 42 | field("Undock Counter") { 43 | label(undockCounterProperty) { 44 | style { 45 | fontSize = 48.pt 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | override fun onDock() { 54 | super.onDock() 55 | dockCounter++ 56 | println("$tag.dockCounter: $dockCounter") 57 | } 58 | 59 | override fun onUndock() { 60 | super.onUndock() 61 | undockCounter++ 62 | println("$tag.undockCounter: $undockCounter") 63 | } 64 | } 65 | 66 | class SecondaryDockingView : PrimaryDockingView() { 67 | 68 | override val tag: String 69 | get() = "SecondaryDockingView" 70 | } 71 | 72 | class NoPrimaryViewDockingApp : App() { 73 | 74 | override fun start(stage: Stage) { 75 | super.start(stage) 76 | stage.scene = Scene(find().root) 77 | stage.show() 78 | } 79 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/testapps/WizardTestApp.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.testapps 2 | 3 | import javafx.collections.FXCollections 4 | import javafx.geometry.Pos 5 | import javafx.scene.control.Button 6 | import javafx.scene.control.ButtonBar 7 | import javafx.scene.input.KeyCode 8 | import javafx.scene.input.KeyEvent 9 | import tornadofx.* 10 | import tornadofx.tests.Customer 11 | import tornadofx.tests.CustomerModel 12 | 13 | class WizardTestApp : WorkspaceApp(WizardCustomerList::class) 14 | 15 | class CustomerWizard : Wizard("Create customer", "Provide customer information") { 16 | val customer: CustomerModel by inject() 17 | 18 | override val canGoNext = currentPageComplete 19 | override val canFinish = allPagesComplete 20 | 21 | init { 22 | add() 23 | add() 24 | customer.item = Customer() 25 | graphic = resources.imageview("/tornadofx/tests/person.png") 26 | showSteps = true 27 | stepLinksCommits = true 28 | showHeader = true 29 | numberedSteps = true 30 | showStepsHeader = false 31 | enableStepLinks = true 32 | enterProgresses = true 33 | } 34 | 35 | override fun onCancel() { 36 | confirm("Confirm cancel", "Do you really want to loose your progress?") { 37 | cancel() 38 | } 39 | } 40 | } 41 | 42 | class WizardStep1 : View("Customer Data") { 43 | val customer: CustomerModel by inject() 44 | val wizard: CustomerWizard by inject() 45 | 46 | override val complete = customer.valid(customer.name) 47 | 48 | override val root = form { 49 | fieldset(title) { 50 | field("Type") { 51 | combobox(customer.type, Customer.Type.values().toList()) 52 | } 53 | field("Name") { 54 | textfield(customer.name) { 55 | required() 56 | // Make sure we get focus instead of the type combo 57 | whenDocked { requestFocus() } 58 | } 59 | } 60 | hbox { 61 | alignment = Pos.BASELINE_RIGHT 62 | hyperlink("Skip") { 63 | action { 64 | wizard.currentPage = wizard.pages.last() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | class WizardStep2 : View("Address") { 73 | val customer: CustomerModel by inject() 74 | override val complete = customer.valid(customer.zip, customer.city) 75 | 76 | override val root = form { 77 | fieldset(title) { 78 | field("Zip/City") { 79 | textfield(customer.zip) { 80 | prefColumnCount = 5 81 | required() 82 | } 83 | textarea(customer.city) { 84 | required() 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | class WizardCustomerList : View() { 92 | val customers = FXCollections.observableArrayList() 93 | 94 | override val root = tableview(customers) { 95 | column("Name", Customer::nameProperty) 96 | columnResizePolicy = SmartResize.POLICY 97 | } 98 | 99 | override fun onDock() { 100 | with(workspace) { 101 | button("_Add").action { 102 | find { 103 | onComplete { customers.add(customer.item) } 104 | openModal() 105 | } 106 | } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/tests/AsyncTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.tests 2 | 3 | import javafx.scene.control.Button 4 | import javafx.scene.layout.BorderPane 5 | import javafx.scene.layout.Pane 6 | import javafx.scene.layout.StackPane 7 | import javafx.stage.Stage 8 | import org.junit.Assert 9 | import org.junit.Test 10 | import org.testfx.api.FxToolkit 11 | import tornadofx.* 12 | import java.util.concurrent.CountDownLatch 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertFalse 15 | import kotlin.test.assertNotEquals 16 | import kotlin.test.assertTrue 17 | 18 | class AsyncTest { 19 | val primaryStage: Stage = FxToolkit.registerPrimaryStage() 20 | 21 | @Test 22 | fun runAsyncWithOverlay() { 23 | //Initially container has embedded node 24 | val container = BorderPane() 25 | val node = Pane() 26 | val mask = Pane() 27 | 28 | container.center = node 29 | assertEquals(container.center, node) 30 | 31 | //This latch will be used to control asynchronously executed task, it's a part of the test 32 | val latch = Latch() 33 | assertTrue(latch.locked) 34 | 35 | //This is a standard CountDownLatch and will be used to synchronize test thread with application thread 36 | val appThreadLatch = CountDownLatch(1) 37 | 38 | //ui is needed here because only after task is executed we can check if overlay is removed propertly 39 | //and it can only be used from within a UIComponent, so we need a temporary object 40 | val component = object : Controller() { 41 | fun run() = node.runAsyncWithOverlay(latch, null, mask) ui { appThreadLatch.countDown() } 42 | } 43 | component.run() 44 | 45 | //Until latch is released, a node is replaced with StackPane containing both the original component and the mask 46 | assertNotEquals(node, container.center) 47 | assertTrue(container.center is StackPane) 48 | assertTrue((container.center as StackPane).children.containsAll(setOf(node, mask))) 49 | 50 | //The working thread (test thread in this case) proceeds, which should result in removing overlay 51 | latch.release() 52 | assertFalse(latch.locked) 53 | 54 | //Waiting until we have confirmed post-task ui execution 55 | appThreadLatch.await() 56 | 57 | //At this point overlay should be removed and node should be again in its original place 58 | assertEquals(node, container.center) 59 | } 60 | 61 | @Test 62 | fun latch() { 63 | //Latch set for 5 concurrent tasks 64 | val count = 5 65 | val latch = Latch(count) 66 | val button = Button() 67 | 68 | assertFalse(button.disabledProperty().value) 69 | 70 | //Button should stay disabled until all tasks are over 71 | button.disableWhen(latch.lockedProperty()) 72 | 73 | (1..count).forEach { 74 | assertTrue(button.disabledProperty().value) 75 | assertTrue(latch.locked) 76 | assertTrue(latch.lockedProperty().value) 77 | latch.countDown() 78 | //Latch count should decrease after each iteration 79 | Assert.assertEquals(count, (it + latch.count).toInt()) 80 | } 81 | 82 | assertFalse(button.disabledProperty().value) 83 | assertFalse(latch.locked) 84 | 85 | //Should have no effect anyway 86 | latch.release() 87 | assertFalse(button.disabledProperty().value) 88 | assertFalse(latch.locked) 89 | } 90 | 91 | @Test 92 | fun runAsync() { 93 | tornadofx.runAsync(daemon = true) { 94 | assertTrue { Thread.currentThread().isDaemon } 95 | } 96 | tornadofx.runAsync { 97 | assertFalse { Thread.currentThread().isDaemon } 98 | } 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/tests/BindingTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.tests 2 | 3 | import javafx.beans.binding.BooleanExpression 4 | import javafx.beans.property.SimpleBooleanProperty 5 | import javafx.beans.property.SimpleDoubleProperty 6 | import javafx.collections.FXCollections 7 | import javafx.scene.control.Label 8 | import javafx.stage.Stage 9 | import org.junit.Assert 10 | import org.junit.Test 11 | import org.testfx.api.FxToolkit 12 | import tornadofx.* 13 | import java.text.DecimalFormat 14 | import java.text.DecimalFormatSymbols 15 | import java.util.* 16 | 17 | class BindingTest { 18 | val primaryStage: Stage = FxToolkit.registerPrimaryStage() 19 | 20 | @Test 21 | fun testBindingFormat() { 22 | val property = SimpleDoubleProperty(Math.PI) 23 | 24 | val label = Label() 25 | label.bind(property, format = DecimalFormat("#0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH))) 26 | 27 | Assert.assertEquals("3.1", label.text) 28 | } 29 | 30 | @Test 31 | fun observableListBinding() { 32 | val elements = FXCollections.observableArrayList("Hello", "World") 33 | val binding = stringBinding(elements, elements) { this.joinToString(" ")} 34 | val uielement = Label().apply { bind(binding) } 35 | Assert.assertEquals("Hello World", uielement.text) 36 | elements.setAll("Hello", "Changed") 37 | Assert.assertEquals("Hello Changed", uielement.text) 38 | } 39 | 40 | @Test 41 | fun observableMapBinding() { 42 | val map = FXCollections.observableHashMap() 43 | val list = observableListOf() 44 | map["0123456"] = 3 45 | list.bind(map){k,v-> k[v]} 46 | Assert.assertEquals(list, listOf('3')) 47 | map["0123456"] = 4 48 | Assert.assertEquals(list, listOf('4')) 49 | map["abcdefg"] = 3 50 | Assert.assertTrue( 51 | "expected ['d', '4'] got ${list.joinToString() }}", 52 | list.containsAll(listOf('d', '4')) && list.size == 2 53 | ) 54 | } 55 | 56 | @Test 57 | fun nestedBinding() { 58 | val father = Person("Mr Father", 50) 59 | val stepFather = Person("Mr Step Father", 40) 60 | val child = Person("Firstborn Child", 18) 61 | child.parent = father 62 | 63 | val fatherName = child.parentProperty().select(Person::nameProperty) 64 | Assert.assertEquals("Mr Father", fatherName.value) 65 | fatherName.value = "Mister Father" 66 | Assert.assertEquals("Mister Father", father.name) 67 | child.parent = stepFather 68 | Assert.assertEquals("Mr Step Father", fatherName.value) 69 | } 70 | 71 | @Test 72 | fun booleanBinding() { 73 | val mylist = FXCollections.observableArrayList() 74 | val complete = booleanBinding(mylist) { isNotEmpty() } 75 | Assert.assertFalse(complete.value) 76 | mylist.add("One") 77 | Assert.assertTrue(complete.value) 78 | } 79 | 80 | @Test 81 | fun booleanListBinding() { 82 | val mylist = FXCollections.observableArrayList() 83 | val complete = booleanListBinding(mylist) { this } 84 | Assert.assertFalse(complete.value) 85 | mylist.add(SimpleBooleanProperty(true)) 86 | Assert.assertTrue(complete.value) 87 | mylist.add(SimpleBooleanProperty(false)) 88 | Assert.assertFalse(complete.value) 89 | } 90 | } -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/tests/BuildersTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.tests 2 | 3 | import javafx.scene.layout.StackPane 4 | import javafx.scene.text.Text 5 | import javafx.stage.Stage 6 | import org.hamcrest.Matchers 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.testfx.api.FxAssert.verifyThat 10 | import org.testfx.api.FxToolkit 11 | import org.testfx.matcher.control.TextMatchers 12 | import tornadofx.* 13 | 14 | class BuildersTest { 15 | val primaryStage: Stage = FxToolkit.registerPrimaryStage() 16 | 17 | lateinit var pane: StackPane 18 | 19 | @Before 20 | fun setup() { 21 | pane = StackPane() 22 | } 23 | 24 | @Test 25 | fun text_builder() { 26 | // expect: 27 | verifyThat(pane.text(), Matchers.instanceOf(Text::class.java)) 28 | verifyThat(pane.text(), TextMatchers.hasText("")) 29 | verifyThat(pane.text("foo"), TextMatchers.hasText("foo")) 30 | verifyThat(pane.text() { text = "bar" }, TextMatchers.hasText("bar")) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/kotlin/tornadofx/tests/CSSTest.kt: -------------------------------------------------------------------------------- 1 | package tornadofx.tests 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import javafx.collections.FXCollections 5 | import javafx.scene.Scene 6 | import javafx.scene.control.Label 7 | import javafx.scene.layout.StackPane 8 | import javafx.stage.Stage 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.assertj.core.api.SoftAssertions 11 | import org.junit.Test 12 | import org.testfx.api.FxAssert 13 | import org.testfx.api.FxService 14 | import org.testfx.api.FxToolkit 15 | import org.testfx.matcher.base.NodeMatchers 16 | import tornadofx.* 17 | 18 | class CSSTest { 19 | val primaryStage: Stage = FxToolkit.registerPrimaryStage() 20 | 21 | class Styles: Stylesheet() { 22 | companion object { 23 | val tcss by cssclass() 24 | } 25 | } 26 | 27 | @Test 28 | fun `addClass should add only unique classes`() { 29 | val testValue0 = SimpleIntegerProperty(0) 30 | val list = FXCollections.observableArrayList(testValue0) 31 | 32 | FxToolkit.setupFixture { 33 | val root = StackPane().apply { 34 | label("test label") 35 | } 36 | primaryStage.scene = Scene(root) 37 | primaryStage.show() 38 | } 39 | //precheck 40 | val label = FxService.serviceContext().nodeFinder.lookup(".label").query