├── project
├── build.properties
└── plugins.sbt
├── .gitignore
├── demo
└── src
│ └── main
│ ├── resources
│ └── scalafxml
│ │ └── demo
│ │ ├── localization.properties
│ │ ├── localization_sv.properties
│ │ ├── nested
│ │ ├── nested.fxml
│ │ └── window.fxml
│ │ ├── startscreen.fxml
│ │ ├── getcontroller
│ │ └── unitconverter.fxml
│ │ ├── unitconverter
│ │ └── unitconverter.fxml
│ │ └── thirdparty
│ │ └── unitconverter.fxml
│ └── scala
│ └── scalafxml
│ └── demo
│ ├── nested
│ ├── WindowController.scala
│ ├── NestedDemo.scala
│ └── NestedController.scala
│ ├── unitconverter
│ ├── UnitConverter.scala
│ ├── ScalaFXML.scala
│ ├── PureScalaFX.scala
│ └── RefactoredPureScalaFX.scala
│ ├── SimpleDemo.scala
│ ├── StartScreenPresenter.scala
│ ├── MacWireDemo.scala
│ ├── GuiceDemo.scala
│ ├── thirdparty
│ └── ThirdPartyControlsDemo.scala
│ └── getcontroller
│ └── GetControllerDemo.scala
├── .bsp
└── sbt.json
├── core
└── src
│ ├── test
│ └── scala
│ │ └── ProxyGeneratorTest.scala
│ └── main
│ └── scala
│ └── scalafxml
│ └── core
│ ├── ControllerAccessor.scala
│ ├── FXMLView.scala
│ ├── FXMLLoader.scala
│ ├── FxmlProxyGenerator.scala
│ └── ControllerDependencyResolver.scala
├── subcut
└── src
│ └── main
│ └── scala
│ └── scalafxml
│ └── subcut
│ ├── SubCutDependencyResolver.scala
│ └── SubCutHelper.scala
├── macwire
└── src
│ └── main
│ └── scala
│ └── scalafxml
│ └── macwire
│ └── MacWireDependencyResolver.scala
├── guice
└── src
│ └── main
│ └── scala
│ └── scalafxml
│ └── guice
│ └── GuiceDependencyResolver.scala
├── .github
└── workflows
│ ├── ci.yaml
│ └── release.yaml
├── README.md
└── core-macros
└── src
└── main
└── scala
└── scalafxml
└── core
└── macros
└── sfxmlMacro.scala
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.9.9
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea
3 | *.iml
4 | *.DS_Store
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/localization.properties:
--------------------------------------------------------------------------------
1 | TEXT_CREATE_NEW_PHOTO_BOOK=Create new photo book
2 | TEXT_OPEN_EXISTING_PHOTO_BOOK=Open existing photo book
3 | TEXT_BROWSE=Browse
4 | TEXT_CREATE=Create
5 | TEXT_HELLO_WORLD=Hello World
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/localization_sv.properties:
--------------------------------------------------------------------------------
1 | TEXT_CREATE_NEW_PHOTO_BOOK=Skapa ett nytt album
2 | TEXT_OPEN_EXISTING_PHOTO_BOOK=\u00D6ppna ett album
3 | TEXT_BROWSE=Bl\u00E4ddra
4 | TEXT_CREATE=Skapa
5 | TEXT_HELLO_WORLD=Hejsan V\u00E4rlden
--------------------------------------------------------------------------------
/.bsp/sbt.json:
--------------------------------------------------------------------------------
1 | {"name":"sbt","version":"1.9.9","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/vigoo/.sdkman/candidates/java/21-zulu/zulu-21.jdk/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/vigoo/.sdkman/candidates/sbt/1.9.6/bin/sbt-launch.jar","-Dsbt.script=/Users/vigoo/.sdkman/candidates/sbt/current/bin/sbt","xsbt.boot.Boot","-bsp"]}
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "sonatype-releases" at "https://oss.sonatype.org/content/repositories/releases/"
2 |
3 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
4 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7")
5 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
6 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.0")
7 |
8 |
--------------------------------------------------------------------------------
/core/src/test/scala/ProxyGeneratorTest.scala:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import org.scalatest._
4 | import scala.reflect.runtime.universe._
5 | import scalafxml.core.FxmlProxyGenerator
6 | import FxmlProxyGenerator._
7 | import org.scalatest.flatspec.AnyFlatSpec
8 | import org.scalatest.matchers.should.Matchers
9 |
10 | class ProxyGeneratorTest extends AnyFlatSpec with Matchers {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/nested/nested.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/core/src/main/scala/scalafxml/core/ControllerAccessor.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core
2 |
3 | /**
4 | * Provides access for a wrapped controller.
5 | *
6 | * Implemented by the macro-generated controller classes.
7 | */
8 | trait ControllerAccessor {
9 |
10 | /**
11 | * Gets the controller implementation casted to the given type
12 | * @tparam T a supertype of the original controller class
13 | * @return returns the original controller instance
14 | */
15 | def as[T](): T
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/nested/WindowController.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.nested
2 |
3 | import scalafx.scene.layout.VBox
4 | import scalafxml.core.macros.{nested, sfxml}
5 |
6 | @sfxml
7 | class WindowController(nested: VBox,
8 | @nested[NestedController] nestedController: NestedControllerInterface) {
9 |
10 | println(s"Window controller initialized with nested control $nested and controller $nestedController")
11 | nestedController.doSomething()
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/nested/NestedDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.nested
2 |
3 | import scalafx.application.JFXApp
4 | import scalafx.scene.Scene
5 | import scalafx.Includes._
6 | import scalafxml.core.{DependenciesByType, FXMLView}
7 |
8 | object NestedDemo extends JFXApp {
9 |
10 | val root = FXMLView(getClass.getResource("window.fxml"),
11 | new DependenciesByType(Map.empty))
12 |
13 | stage = new JFXApp.PrimaryStage() {
14 | title = "Nested controllers demo"
15 | scene = new Scene(root)
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/nested/NestedController.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.nested
2 |
3 | import scalafx.scene.control.Label
4 | import scalafxml.core.macros.sfxml
5 |
6 | trait NestedControllerInterface {
7 | def doSomething(): Unit
8 | }
9 |
10 | @sfxml
11 | class NestedController(label: Label) extends NestedControllerInterface {
12 |
13 | println(s"Nested controller initialized with label: $label")
14 |
15 | override def doSomething(): Unit = {
16 | label.text = "Nested controller called!"
17 | println("Nested controller called")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/subcut/src/main/scala/scalafxml/subcut/SubCutDependencyResolver.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.subcut
2 |
3 | import com.escalatesoft.subcut.inject.BindingModule
4 | import scala.reflect.runtime.universe.Type
5 | import scalafxml.core.ControllerDependencyResolver
6 | import scalafxml.subcut.SubCutHelper._
7 |
8 | /** SubCut based dependency resolver for ScalaFXML controllers */
9 | class SubCutDependencyResolver(implicit val bindingModule: BindingModule) extends ControllerDependencyResolver {
10 |
11 | def get(paramName: String, dependencyType: Type): Option[Any] = {
12 | injectOptional(bindingModule, dependencyType)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/macwire/src/main/scala/scalafxml/macwire/MacWireDependencyResolver.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.macwire
2 |
3 | import com.softwaremill.macwire.Wired
4 | import scala.reflect.runtime.universe._
5 | import scalafxml.core.ControllerDependencyResolver
6 |
7 | /** MacWire based dependency resolver for ScalaFXML controllers */
8 | class MacWireDependencyResolver(wired: Wired) extends ControllerDependencyResolver {
9 |
10 | def get(paramName: String, dependencyType: Type): Option[Any] = {
11 | val rm = runtimeMirror(getClass.getClassLoader)
12 | val cls = Class.forName(rm.runtimeClass(dependencyType).getName)
13 | try {
14 | Some(wired.lookupSingleOrThrow(cls))
15 | } catch {
16 | case _: Throwable => None
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/guice/src/main/scala/scalafxml/guice/GuiceDependencyResolver.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.guice
2 |
3 | import scala.reflect.runtime.universe._
4 |
5 | import scalafxml.core.ControllerDependencyResolver
6 | import com.google.inject.Injector
7 |
8 | import scala.util.{Try, Failure, Success}
9 |
10 | /** Guice based dependency resolver for ScalaFXML controllers */
11 | class GuiceDependencyResolver(implicit val injector: Injector) extends ControllerDependencyResolver {
12 |
13 | def get(paramName: String, dependencyType: Type): Option[Any] = {
14 | val rm = runtimeMirror(getClass.getClassLoader)
15 | val cls = Class.forName(rm.runtimeClass(dependencyType).getName)
16 | Try(injector.getInstance(cls)) match {
17 | case Success(instance) => Some(instance)
18 | case Failure(_) => None
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/unitconverter/UnitConverter.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.unitconverter
2 |
3 | trait UnitConverter {
4 | val description: String
5 | def run(input: String): String
6 |
7 | override def toString = description
8 | }
9 |
10 | class UnitConverters(converters: UnitConverter*) {
11 | val available = List(converters : _*)
12 | }
13 |
14 | object MMtoInches extends UnitConverter {
15 | val description: String = "Millimeters to inches"
16 | def run(input: String): String = try { (input.toDouble / 25.4).toString } catch { case ex: Throwable => ex.toString }
17 | }
18 |
19 | object InchesToMM extends UnitConverter {
20 | val description: String = "Inches to millimeters"
21 | def run(input: String): String = try { (input.toDouble * 25.4).toString } catch { case ex: Throwable => ex.toString }
22 | }
--------------------------------------------------------------------------------
/core/src/main/scala/scalafxml/core/FXMLView.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core
2 |
3 | import javafx.{scene => jfxs}
4 | import java.net.URL
5 | import java.util.ResourceBundle
6 |
7 | /** Factory for FXML based views */
8 | object FXMLView {
9 |
10 | /** Creates the JavaFX node representing the control described in FXML
11 | *
12 | * @param fxml URL to the FXML to be loaded
13 | * @param dependencies dependency resolver for finding non-bound dependencies
14 | * @param bundle optional bundle for localization resources.
15 | * @return the JavaFX node
16 | */
17 | def apply(fxml: URL, dependencies: ControllerDependencyResolver, bundle : Option[ResourceBundle] = None): jfxs.Parent = {
18 | val loader = new FXMLLoader(fxml, dependencies, bundle)
19 | loader.load()
20 | loader.getRoot[jfxs.Parent]()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/SimpleDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo
2 |
3 | import java.util.{Locale, MissingResourceException, ResourceBundle}
4 |
5 | import scalafx.application.JFXApp
6 | import scalafx.Includes._
7 | import scalafx.scene.Scene
8 | import scala.reflect.runtime.universe.typeOf
9 | import scalafxml.core.{DependenciesByType, FXMLView}
10 |
11 | object SimpleDemo extends JFXApp {
12 | val resourceBundle = ResourceBundle.getBundle("scalafxml.demo.Localization", new Locale("sv", "SE"))
13 |
14 | val root = FXMLView(getClass.getResource("startscreen.fxml"),
15 | new DependenciesByType(Map(
16 | typeOf[TestDependency] -> new TestDependency("hello world"))),
17 | Some(resourceBundle))
18 |
19 | stage = new JFXApp.PrimaryStage() {
20 | title = resourceBundle.getString("TEXT_HELLO_WORLD")
21 | scene = new Scene(root)
22 |
23 | }
24 | }
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/StartScreenPresenter.scala:
--------------------------------------------------------------------------------
1 |
2 | package scalafxml.demo
3 |
4 | import scalafx.scene.control.TextField
5 | import scalafx.scene.control.Button
6 | import scalafx.scene.control.ListView
7 | import scalafx.event.ActionEvent
8 | import scalafxml.core.macros.sfxml
9 |
10 | case class TestDependency(initialPath: String)
11 |
12 | @sfxml
13 | class StartScreenPresenter(
14 | newPhotoBookPath: TextField,
15 | btCreate: Button,
16 | recentPaths: ListView[String],
17 | testDep: TestDependency) {
18 |
19 | println(s"testDep is $testDep")
20 |
21 | newPhotoBookPath.text = testDep.initialPath
22 |
23 | def onBrowse(event: ActionEvent) {
24 | println(newPhotoBookPath.text)
25 | println("onBrowse")
26 | }
27 |
28 | def onBrowseForOpen(event: ActionEvent) {
29 | println("onBrowseForOpen")
30 | }
31 |
32 | def onCreate(event: ActionEvent) {
33 | println("onCreate")
34 | }
35 | }
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/MacWireDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo
2 |
3 | import java.util.{MissingResourceException, ResourceBundle}
4 |
5 | import scalafx.application.JFXApp
6 | import scalafx.scene.Scene
7 | import scalafx.Includes._
8 | import scalafxml.core.FXMLView
9 | import scalafxml.macwire.MacWireDependencyResolver
10 | import com.softwaremill.macwire._
11 |
12 | object MacWireDemo extends JFXApp {
13 |
14 | class Module {
15 | def testDependency = TestDependency("MacWire dependency")
16 | }
17 |
18 | lazy val wired: Wired = wiredInModule(new Module)
19 |
20 | stage = new JFXApp.PrimaryStage() {
21 | val resourceBundle = ResourceBundle.getBundle("scalafxml.demo.Localization")
22 |
23 | title = resourceBundle.getString("TEXT_HELLO_WORLD")
24 | scene = new Scene(FXMLView(getClass.getResource("startscreen.fxml"), new MacWireDependencyResolver(wired), Some(resourceBundle)))
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/GuiceDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo
2 |
3 | import java.util.{Locale, MissingResourceException, ResourceBundle}
4 |
5 | import scalafx.Includes._
6 | import scalafx.application.JFXApp
7 | import scalafx.scene.Scene
8 | import scalafxml.core.FXMLView
9 | import scalafxml.guice.GuiceDependencyResolver
10 | import com.google.inject.{AbstractModule, Guice}
11 |
12 | object GuiceDemo extends JFXApp {
13 |
14 | val module = new AbstractModule {
15 | override def configure() {
16 | bind(classOf[TestDependency]).toInstance(new TestDependency("guice dependency"))
17 | }
18 | }
19 | implicit val injector = Guice.createInjector(module)
20 |
21 | stage = new JFXApp.PrimaryStage() {
22 | val resourceBundle = ResourceBundle.getBundle("scalafxml.demo.Localization")
23 | title = resourceBundle.getString("TEXT_HELLO_WORLD")
24 |
25 | scene = new Scene(FXMLView(getClass.getResource("startscreen.fxml"), new GuiceDependencyResolver(), Some(resourceBundle)))
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/src/main/scala/scalafxml/core/FXMLLoader.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core
2 |
3 | import java.net.URL
4 | import java.util.ResourceBundle
5 | import javafx.{fxml => jfxf}
6 | import javafx.{util => jfxu}
7 |
8 | /**
9 | * Extends the JavaFX [[javafx.fxml.FXMLLoader]] to support ScalaFXML controller classes
10 | *
11 | * The [[scalafxml.core.FXMLLoader.getController()]] method is overridden to work with
12 | * the original, wrapped controller instances.
13 | *
14 | * @param fxml URL to the FXML to be loaded
15 | * @param dependencies dependency resolver for finding non-bound dependencies
16 | * @param bundle optional bundle for localization resources
17 | */
18 | class FXMLLoader(fxml: URL, dependencies: ControllerDependencyResolver, bundle : Option[ResourceBundle] = None)
19 | extends jfxf.FXMLLoader(
20 | fxml,
21 | bundle.orNull,
22 | new jfxf.JavaFXBuilderFactory(),
23 | new jfxu.Callback[Class[_], Object] {
24 | override def call(cls: Class[_]): Object =
25 | FxmlProxyGenerator(cls, dependencies)
26 | }) {
27 |
28 | override def getController[T](): T = super.getController[ControllerAccessor].as[T]
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | pull_request:
5 | jobs:
6 | build-test:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | scala: ['2.11.12', '2.12.14', '2.13.6']
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 | - uses: actions/setup-java@v2
16 | with:
17 | distribution: 'zulu'
18 | java-version: '11'
19 | java-package: jdk+fx
20 | - name: Setup SBT
21 | shell: bash
22 | run: |
23 | # update this only when sbt-the-bash-script needs to be updated
24 | export SBT_LAUNCHER=1.5.5
25 | export SBT_OPTS="-Dfile.encoding=UTF-8"
26 | curl -L --silent "https://github.com/sbt/sbt/releases/download/v$SBT_LAUNCHER/sbt-$SBT_LAUNCHER.tgz" > $HOME/sbt.tgz
27 | tar zxf $HOME/sbt.tgz -C $HOME
28 | sudo rm -f /usr/local/bin/sbt
29 | sudo ln -s $HOME/sbt/bin/sbt /usr/local/bin/sbt
30 | - name: Coursier cache
31 | uses: coursier/cache-action@v5
32 | - name: Build and test
33 | run: sbt ++${{ matrix.scala }} clean test
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches: ['master']
5 | release:
6 | types:
7 | - published
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 | - uses: actions/setup-java@v2
17 | with:
18 | distribution: 'zulu'
19 | java-version: '11'
20 | java-package: jdk+fx
21 | - name: Setup SBT
22 | shell: bash
23 | run: |
24 | # update this only when sbt-the-bash-script needs to be updated
25 | export SBT_LAUNCHER=1.5.5
26 | export SBT_OPTS="-Dfile.encoding=UTF-8"
27 | curl -L --silent "https://github.com/sbt/sbt/releases/download/v$SBT_LAUNCHER/sbt-$SBT_LAUNCHER.tgz" > $HOME/sbt.tgz
28 | tar zxf $HOME/sbt.tgz -C $HOME
29 | sudo rm -f /usr/local/bin/sbt
30 | sudo ln -s $HOME/sbt/bin/sbt /usr/local/bin/sbt
31 | - uses: olafurpg/setup-gpg@v3
32 | - run: sbt ci-release
33 | env:
34 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
35 | PGP_SECRET: ${{ secrets.PGP_SECRET }}
36 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
37 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
--------------------------------------------------------------------------------
/subcut/src/main/scala/scalafxml/subcut/SubCutHelper.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.subcut
2 |
3 | import scala.reflect.runtime.{universe => ru}
4 | import com.escalatesoft.subcut.inject.BindingModule
5 |
6 | /** Helper class for [[scalafxml.subcut.SubCutDependencyResolver]] */
7 | object SubCutHelper {
8 |
9 | /** Invokes dynamically the SubCut binding module's injectOptional method
10 | *
11 | * @param bindingModule binding module to invoke
12 | * @param dependencyType type to pass to injectOptional as a type argument
13 | * @return returns the result of the invoked method
14 | */
15 | def injectOptional(bindingModule: BindingModule, dependencyType: ru.Type): Option[Any] = {
16 | import ru._
17 |
18 | val rm = runtimeMirror(bindingModule.getClass.getClassLoader)
19 | val instanceMirror = rm.reflect(bindingModule)
20 | val injectOptionalSymbols = typeOf[BindingModule].decl(TermName("injectOptional")).asTerm.alternatives
21 | val injectOptionalSym = injectOptionalSymbols.filter {
22 | case m: MethodSymbol => {
23 | m.paramLists.size == 2 &&
24 | m.paramLists(0).size == 1 &&
25 | m.paramLists(0)(0).typeSignature =:= typeOf[Option[String]]
26 | }
27 | }.head.asMethod
28 |
29 | val methodMirror = instanceMirror.reflectMethod(injectOptionalSym)
30 | methodMirror.apply(None, Manifest.classType(Class.forName(rm.runtimeClass(dependencyType).getName))).asInstanceOf[Option[Any]]
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/unitconverter/ScalaFXML.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.unitconverter
2 |
3 | import scala.reflect.runtime.universe.typeOf
4 | import scalafx.application.{Platform, JFXApp}
5 | import scalafx.Includes._
6 | import scalafx.scene.Scene
7 | import scalafx.scene.control.{ComboBox, TextField}
8 | import scalafx.event.ActionEvent
9 | import scalafxml.core.{DependenciesByType, FXMLView}
10 | import scalafxml.core.macros.sfxml
11 | import javafx.beans.binding.StringBinding
12 |
13 | @sfxml
14 | class UnitConverterPresenter(from: TextField,
15 | to: TextField,
16 | types: ComboBox[UnitConverter],
17 | converters: UnitConverters) {
18 |
19 | // Filling the combo box
20 | for (converter <- converters.available) {
21 | types += converter
22 | }
23 | types.getSelectionModel.selectFirst()
24 |
25 | // Data binding
26 | to.text <== new StringBinding {
27 | bind(from.text.delegate, types.getSelectionModel.selectedItemProperty)
28 |
29 | def computeValue() = types.getSelectionModel.getSelectedItem.run(from.text.value)
30 | }
31 |
32 | // Close button event handler
33 | def onClose(event: ActionEvent) {
34 | Platform.exit()
35 | }
36 | }
37 |
38 | object ScalaFXML extends JFXApp {
39 |
40 | val root = FXMLView(getClass.getResource("unitconverter.fxml"),
41 | new DependenciesByType(Map(
42 | typeOf[UnitConverters] -> new UnitConverters(InchesToMM, MMtoInches))))
43 |
44 | stage = new JFXApp.PrimaryStage() {
45 | title = "Unit conversion"
46 | scene = new Scene(root)
47 |
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/startscreen.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/core/src/main/scala/scalafxml/core/FxmlProxyGenerator.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core
2 |
3 | import scala.reflect.runtime.universe._
4 | import scala.reflect.runtime.universe.Flag._
5 |
6 | /** Proxy generator for FXML controllers
7 | *
8 | * In the dynamic proxy version this object was responsible
9 | * for generating the proxy. In this static, compile-time
10 | * version its purpose is to pass the dependency resolver
11 | * to the dynamically instantiated controller class.
12 | */
13 | object FxmlProxyGenerator {
14 |
15 | /** Trait implemented by the generated proxies for supporting dependency injection */
16 | trait ProxyDependencyInjection {
17 |
18 | /** Mutable map for storing a name->value pairs of dependencies */
19 | val deps = scala.collection.mutable.Map[String, Any]()
20 |
21 | /** Injects a dependency
22 | *
23 | * @param paramName name of the constructor argument in the controller
24 | * @param value value to inject to the controller
25 | */
26 | def setDependency(paramName: String, value: Any): Unit = {
27 | deps.put(paramName, value)
28 | }
29 |
30 | /** Gets an injected dependency
31 | *
32 | * @param paramName name of the constructor argument in the controller
33 | * @return the injected value or null
34 | */
35 | def getDependency[T](paramName: String): T = deps.getOrElse(paramName, null).asInstanceOf[T]
36 | }
37 |
38 | /** Creates a new controller proxy instance
39 | *
40 | * @param typ type of the controller class (coming from the FXML)
41 | * @param dependencyResolver dependency resolver for finding non-FXML-bound controller dependencies
42 | * @return returns the proxy for the controller
43 | */
44 | def apply(typ: Class[_], dependencyResolver: ControllerDependencyResolver = NoDependencyResolver): Object = {
45 |
46 | val proxy = typ.getConstructor(classOf[ControllerDependencyResolver]).newInstance(dependencyResolver)
47 | proxy.asInstanceOf[Object]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/scala/scalafxml/core/ControllerDependencyResolver.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core
2 |
3 | import scala.reflect.runtime.universe.Type
4 |
5 | /** Dependency resolver interface for controller proxies
6 | *
7 | * The ScalaFXML controller classes use constructor injection to
8 | * get the FXML-bound controls and some additional dependencies.
9 | * These additional dependencies are resolved through this interface
10 | * at runtime.
11 | *
12 | * The resolvers get both the constructor argument's name and its
13 | * type.
14 | */
15 | trait ControllerDependencyResolver {
16 |
17 | /** Resolves a dependency
18 | *
19 | * @param paramName name of the constructor argument
20 | * @param dependencyType type of the constructor argument
21 | * @return returns either some arbitrary value or none if it could not
22 | * resolve the dependency.
23 | */
24 | def get(paramName: String, dependencyType: Type): Option[Any]
25 | }
26 |
27 | /** Default dependency resolver that does not resolve anything */
28 | object NoDependencyResolver extends ControllerDependencyResolver {
29 |
30 | def get(paramName: String, dependencyType: Type): Option[Any] = None
31 | }
32 |
33 | /** Dependency resolver based on the constructor argument's names
34 | *
35 | * @constructor creates a new dependency resolver based on a mapping
36 | * @param deps dependency mapping, from constructor argument names to values
37 | */
38 | class ExplicitDependencies(deps: Map[String, Any]) extends ControllerDependencyResolver {
39 | def get(paramName: String, dependencyType: Type): Option[Any] = deps.get(paramName)
40 | }
41 |
42 | /** Dependency resolver based on the constructor argument's types
43 | *
44 | * @constructor creates a new dependency resolver based on a mapping
45 | * @param deps dependency mapping, from dependency type to values
46 | */
47 | class DependenciesByType(deps: Map[Type, Any]) extends ControllerDependencyResolver {
48 | def get(paramName: String, dependencyType: Type): Option[Any] = deps.get(dependencyType)
49 | }
50 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/thirdparty/ThirdPartyControlsDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.thirdparty
2 |
3 | import scala.reflect.runtime.universe.typeOf
4 | import scalafx.application.{JFXApp, Platform}
5 | import scalafx.Includes._
6 | import scalafx.scene.Scene
7 | import scalafx.scene.control.{ComboBox, TextField}
8 | import scalafx.event.ActionEvent
9 | import scalafxml.core.{DependenciesByType, FXMLView}
10 | import scalafxml.core.macros.sfxml
11 | import javafx.beans.binding.StringBinding
12 | import javafx.fxml.FXML
13 |
14 | import com.jfoenix.controls.JFXTextField
15 |
16 | import scalafxml.demo.unitconverter.{InchesToMM, MMtoInches, UnitConverter, UnitConverters}
17 |
18 | //@sfxml
19 | //class UnitConverterPresenter(@FXML from: JFXTextField,
20 | // @FXML to: JFXTextField,
21 | // types: ComboBox[UnitConverter],
22 | // converters: UnitConverters) {
23 |
24 |
25 | @sfxml(additionalControls=List("com.jfoenix.controls"))
26 | class UnitConverterPresenter(from: JFXTextField,
27 | to: JFXTextField,
28 | types: ComboBox[UnitConverter],
29 | converters: UnitConverters) {
30 |
31 | // Filling the combo box
32 | for (converter <- converters.available) {
33 | types += converter
34 | }
35 | types.getSelectionModel.selectFirst()
36 |
37 | // Data binding
38 | to.text <== new StringBinding {
39 | bind(from.text.delegate, types.getSelectionModel.selectedItemProperty)
40 |
41 | def computeValue() = types.getSelectionModel.getSelectedItem.run(from.text.value)
42 | }
43 |
44 | // Close button event handler
45 | def onClose(event: ActionEvent) {
46 | Platform.exit()
47 | }
48 | }
49 |
50 | object ScalaFXML extends JFXApp {
51 |
52 | val root = FXMLView(getClass.getResource("unitconverter.fxml"),
53 | new DependenciesByType(Map(
54 | typeOf[UnitConverters] -> new UnitConverters(InchesToMM, MMtoInches))))
55 |
56 | stage = new JFXApp.PrimaryStage() {
57 | title = "Unit conversion"
58 | scene = new Scene(root)
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/getcontroller/unitconverter.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/unitconverter/unitconverter.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/unitconverter/PureScalaFX.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.unitconverter
2 |
3 | import scalafx.application.{Platform, JFXApp}
4 | import scalafx.Includes._
5 | import scalafx.scene.Scene
6 | import scalafx.scene.layout.{Priority, ColumnConstraints, GridPane}
7 | import scalafx.scene.control.{Button, TextField, ComboBox, Label}
8 | import scalafx.geometry.{Insets, HPos}
9 | import scalafx.event.ActionEvent
10 | import javafx.beans.binding.StringBinding
11 |
12 | class PureScalaFXView(converters: UnitConverters) extends JFXApp.PrimaryStage {
13 |
14 | // UI Definition
15 | title = "Unit conversion"
16 |
17 | private val types = new ComboBox[UnitConverter]() {
18 | maxWidth = Double.MaxValue
19 | margin = Insets(3)
20 | }
21 |
22 | private val from = new TextField {
23 | margin = Insets(3)
24 | prefWidth = 200.0
25 | }
26 |
27 | private val to = new TextField {
28 | prefWidth = 200.0
29 | margin = Insets(3)
30 | editable = false
31 | }
32 |
33 | scene = new Scene {
34 | content = new GridPane {
35 | padding = Insets(5)
36 |
37 | add(new Label("Conversion type:"), 0, 0)
38 | add(new Label("From:"), 0, 1)
39 | add(new Label("To:"), 0, 2)
40 |
41 | add(types, 1, 0)
42 | add(from, 1, 1)
43 | add(to, 1, 2)
44 |
45 | add(new Button("Close") {
46 | // inline event handler binding
47 | onAction = (e: ActionEvent) => Platform.exit()
48 | }, 1, 3)
49 |
50 | columnConstraints = List(
51 | new ColumnConstraints {
52 | halignment = HPos.Left
53 | hgrow = Priority.Sometimes
54 | margin = Insets(5)
55 | },
56 | new ColumnConstraints {
57 | halignment = HPos.Right
58 | hgrow = Priority.Always
59 | margin = Insets(5)
60 | }
61 | )
62 | }
63 | }
64 |
65 | // Filling the combo box
66 | for (converter <- converters.available) {
67 | types += converter
68 | }
69 | types.getSelectionModel.selectFirst()
70 |
71 | // Data binding
72 | to.text <== new StringBinding {
73 | bind(from.text.delegate, types.getSelectionModel.selectedItemProperty)
74 |
75 | def computeValue() = types.getSelectionModel.getSelectedItem.run(from.text.value)
76 | }
77 | }
78 |
79 | object PureScalaFX extends JFXApp {
80 |
81 | stage = new PureScalaFXView(new UnitConverters(InchesToMM, MMtoInches))
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/thirdparty/unitconverter.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/getcontroller/GetControllerDemo.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.getcontroller
2 |
3 |
4 | import scalafx.application.{Platform, JFXApp}
5 | import scalafx.Includes._
6 | import scalafxml.core.macros.sfxml
7 | import scalafx.scene.Scene
8 | import scala.reflect.runtime.universe.typeOf
9 | import scalafxml.core.DependenciesByType
10 | import scalafxml.core.FXMLLoader
11 | import scalafxml.demo.unitconverter.{MMtoInches, InchesToMM, UnitConverters, UnitConverter}
12 | import scalafx.scene.control.{ComboBox, TextField}
13 | import javafx.beans.binding.StringBinding
14 | import scalafx.event.ActionEvent
15 | import javafx.{scene => jfxs}
16 |
17 | /**
18 | * Public interface of our controller which will be available through FXMLLoader
19 | */
20 | trait UnitConverterInterface {
21 | def setInitialValue(value: Double)
22 | }
23 |
24 | /** Our controller class, implements UnitConverterInterface */
25 | @sfxml
26 | class UnitConverterPresenter(from: TextField,
27 | to: TextField,
28 | types: ComboBox[UnitConverter],
29 | converters: UnitConverters)
30 | extends UnitConverterInterface {
31 |
32 | // Filling the combo box
33 | for (converter <- converters.available) {
34 | types += converter
35 | }
36 | types.getSelectionModel.selectFirst()
37 |
38 | // Data binding
39 | to.text <== new StringBinding {
40 | bind(from.text.delegate, types.getSelectionModel.selectedItemProperty)
41 | def computeValue() = types.getSelectionModel.getSelectedItem.run(from.text.value)
42 | }
43 |
44 | // Close button event handler
45 | def onClose(event: ActionEvent) {
46 | Platform.exit()
47 | }
48 |
49 | def setInitialValue(value: Double) {
50 | from.text = value.toString
51 | }
52 | }
53 |
54 |
55 | object GetControllerDemo extends JFXApp {
56 |
57 | // Instead of FXMLView, we create a new ScalaFXML loader
58 | val loader = new FXMLLoader(getClass.getResource("unitconverter.fxml"),
59 | new DependenciesByType(Map(
60 | typeOf[UnitConverters] -> new UnitConverters(InchesToMM, MMtoInches))))
61 |
62 | // Load the FXML, the controller will be instantiated
63 | loader.load()
64 |
65 | // Get the scene root
66 | val root = loader.getRoot[jfxs.Parent]
67 |
68 | // Get the controller. We cannot use the controller class itself here,
69 | // because it is transformed by the macro - but we can use the trait it
70 | // implements!
71 | val controller = loader.getController[UnitConverterInterface]
72 | controller.setInitialValue(10)
73 |
74 | stage = new JFXApp.PrimaryStage() {
75 | title = "Unit converter"
76 | scene = new Scene(root)
77 | }
78 | }
--------------------------------------------------------------------------------
/demo/src/main/resources/scalafxml/demo/nested/window.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/demo/src/main/scala/scalafxml/demo/unitconverter/RefactoredPureScalaFX.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.demo.unitconverter
2 |
3 | import scalafx.application.{Platform, JFXApp}
4 | import scalafx.Includes._
5 | import scalafx.scene.Scene
6 | import scalafx.scene.layout.{Priority, ColumnConstraints, GridPane}
7 | import scalafx.scene.control.{Button, TextField, ComboBox, Label}
8 | import scalafx.geometry.{Insets, HPos}
9 | import scalafx.event.ActionEvent
10 | import javafx.beans.binding.StringBinding
11 |
12 | class RawUnitConverterPresenter(from: TextField,
13 | to: TextField,
14 | types: ComboBox[UnitConverter],
15 | converters: UnitConverters) {
16 |
17 | // Filling the combo box
18 | for (converter <- converters.available) {
19 | types += converter
20 | }
21 | types.getSelectionModel.selectFirst()
22 |
23 | // Data binding
24 | to.text <== new StringBinding {
25 | bind(from.text.delegate, types.getSelectionModel.selectedItemProperty)
26 |
27 | def computeValue() = types.getSelectionModel.getSelectedItem.run(from.text.value)
28 | }
29 |
30 | // Close button event handler
31 | def onClose(event: ActionEvent) {
32 | Platform.exit()
33 | }
34 | }
35 |
36 | class RefactoredScalaFXView(converters: UnitConverters) extends JFXApp.PrimaryStage {
37 |
38 | // UI Definition
39 | title = "Unit conversion"
40 |
41 | private val types = new ComboBox[UnitConverter]() {
42 | maxWidth = Double.MaxValue
43 | margin = Insets(3)
44 | }
45 |
46 | private val from = new TextField {
47 | margin = Insets(3)
48 | prefWidth = 200.0
49 | }
50 |
51 | private val to = new TextField {
52 | prefWidth = 200.0
53 | margin = Insets(3)
54 | editable = false
55 | }
56 |
57 | private val presenter = new RawUnitConverterPresenter(from, to, types, converters)
58 |
59 | scene = new Scene {
60 | content = new GridPane {
61 | padding = Insets(5)
62 |
63 | add(new Label("Conversion type:"), 0, 0)
64 | add(new Label("From:"), 0, 1)
65 | add(new Label("To:"), 0, 2)
66 |
67 | add(types, 1, 0)
68 | add(from, 1, 1)
69 | add(to, 1, 2)
70 |
71 | add(new Button("Close") {
72 | // inline event handler binding
73 | onAction = (e: ActionEvent) => presenter.onClose(e)
74 | }, 1, 3)
75 |
76 | columnConstraints = List(
77 | new ColumnConstraints {
78 | halignment = HPos.Left
79 | hgrow = Priority.Sometimes
80 | margin = Insets(5)
81 | },
82 | new ColumnConstraints {
83 | halignment = HPos.Right
84 | hgrow = Priority.Always
85 | margin = Insets(5)
86 | }
87 | )
88 | }
89 | }
90 | }
91 |
92 | object RefactoredPureScalaFX extends JFXApp {
93 |
94 | stage = new RefactoredScalaFXView(new UnitConverters(InchesToMM, MMtoInches))
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | scalafxml
2 | =========
3 | [](https://travis-ci.org/vigoo/scalafxml)
4 |
5 |
6 | The [scalafx](http://www.scalafx.org/) library is a great UI DSL that wraps JavaFX classes and provides a nice syntax to work with them from Scala.
7 |
8 | This library bridges [FXML](http://docs.oracle.com/javafx/2/fxml_get_started/why_use_fxml.htm) and [scalafx](https://code.google.com/p/scalafx/) by automatically building proxy classes, enabling a more clear controller syntax.
9 |
10 | ## Status
11 | The `main` branch contains the initial implementation of the _compile time_ proxy generator, which uses [macro annotations](http://docs.scala-lang.org/overviews/macros/annotations.html). This requires the addition of the [macro paradise](http://docs.scala-lang.org/overviews/macros/paradise.html) compiler plugin, but has no runtime dependencies. It depends on [ScalaFX 8](https://github.com/scalafx/scalafx) and _JavaFX 8_.
12 |
13 | The [`SFX-2`](https://github.com/vigoo/scalafxml/tree/SFX-2) branch is the _compile time_ proxy generator for [ScalaFX 2.2](https://github.com/scalafx/scalafx/tree/SFX-2) using _JavaFX 2_.
14 |
15 | On the `dynamic` branch there is the first version of the proxy generator which executes runtime. This has a disadvantage of having `scala-compiler.jar` as a dependency, but has no special compile-time dependencies.
16 |
17 | The latest published version is `0.5`. To use it in SBT with Scala 2.13 add
18 |
19 | ```scala
20 | scalacOptions += "-Ymacro-annotations"
21 |
22 | libraryDependencies += "org.scalafx" %% "scalafxml-core-sfx8" % "0.5"
23 | ```
24 |
25 | for Scala 2.12 and earlier add:
26 | ```scala
27 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
28 |
29 | libraryDependencies += "org.scalafx" %% "scalafxml-core-sfx8" % "0.5"
30 | ```
31 |
32 | ## Example
33 |
34 | The controller's, referenced from the FXML's through the `fx:controller` attribute, can be implemented as simple Scala classes, getting all the bound controls though the constructor:
35 |
36 | ```scala
37 | import scalafx.scene.control.TextField
38 | import scalafx.scene.control.Button
39 | import scalafx.scene.control.ListView
40 | import scalafx.event.ActionEvent
41 | import scalafxml.core.macros.sfxml
42 |
43 | @sfxml
44 | class TestController(input: TextField,
45 | create: Button,
46 | recentInputs: ListView[String],
47 | dep: AnAdditionalDependency) {
48 |
49 | // event handlers are simple public methods:
50 | def onCreate(event: ActionEvent) {
51 | // ...
52 | }
53 | }
54 | ```
55 |
56 | ### Accessing the controller
57 |
58 | As the *controller class* is replaced in compile time by a generated one, we cannot
59 | directly use it to call the controller of our views. Instead we have to define a public
60 | interface for them and then use it as the type given for the `getController` method of `FXMLLoader`.
61 |
62 | The example below shows this:
63 |
64 | ```scala
65 | trait UnitConverterInterface {
66 | def setInitialValue(value: Double)
67 | }
68 |
69 | @sfxml
70 | class UnitConverterPresenter(// ...
71 | )
72 | extends UnitConverterInterface {
73 |
74 | // ...
75 | }
76 |
77 | // Instead of FXMLView, we create a new ScalaFXML loader
78 | val loader = new FXMLLoader(
79 | getClass.getResource("unitconverter.fxml"),
80 | // ...
81 | )
82 |
83 | loader.load()
84 |
85 | val root = loader.getRoot[jfxs.Parent]
86 |
87 | val controller = loader.getController[UnitConverterInterface]
88 | controller.setInitialValue(10)
89 |
90 | stage = new JFXApp.PrimaryStage() {
91 | title = "Unit converter"
92 | scene = new Scene(root)
93 | }
94 | ```
95 |
96 | ### Nested controllers
97 |
98 | Nested controllers can be used in a similar way as described above, by defining a public interface
99 | for them first, using this interface as the type of the injected value in the parent controller, but
100 | explicitly marking the original controller class with a `@nested` annotation.
101 |
102 | The following example demonstrates this:
103 |
104 | ```scala
105 | trait NestedControllerInterface {
106 | def doSomething(): Unit
107 | }
108 |
109 | @sfxml
110 | class NestedController(label: Label) extends NestedControllerInterface {
111 |
112 | println(s"Nested controller initialized with label: $label")
113 |
114 | override def doSomething(): Unit = {
115 | label.text = "Nested controller called!"
116 | println("Nested controller called")
117 | }
118 | }
119 |
120 | @sfxml
121 | class WindowController(nested: VBox,
122 | @nested[NestedController] nestedController: NestedControllerInterface) {
123 |
124 | nestedController.doSomething()
125 | }
126 | ```
127 |
128 | ### Third party control libraries
129 | scalafxml recognizes factory JavaFX and ScalaFX controls, and assumes everything else to be an external non-UI dependency
130 | to be get from a *dependency provider*. When using third party control libraries, there are two possibilities:
131 |
132 | * Listing the third party control package in the `@sfxml` annotation
133 | * Using the `@FXML` annotation for these controls
134 |
135 | The following example shows how to do this with [JFoenix](https://github.com/jfoenixadmin/JFoenix) using the first method:
136 |
137 | ```scala
138 | @sfxml(additionalControls=List("com.jfoenix.controls"))
139 | class TestController(input: JFXTextField,
140 | create: JFXButton)
141 | ```
142 |
143 | or with the second approach:
144 |
145 | ```scala
146 | @sfxml
147 | class TestController(@FXML input: JFXTextField,
148 | @FXML create: JFXButton)
149 | ```
150 |
151 | ### Dependency injection
152 | Beside the JavaFX controls, additional dependencies can be injected to the controller as well. This injection process is extensible.
153 |
154 | #### Simple
155 | It is also possible to simply give the dependencies _by their type_ or _by their name_:
156 |
157 | ```scala
158 | object SimpleDemo extends JFXApp {
159 |
160 | stage = new JFXApp.PrimaryStage() {
161 | title = "Test window"
162 | scene = new Scene(
163 | FXMLView(getClass.getResource("test.fxml"),
164 | new DependenciesByType(Map(
165 | typeOf[AnAdditionalDependency] -> new AnAdditionalDependency("dependency by type"))))
166 |
167 | }
168 | }
169 | ```
170 |
171 | #### SubCut
172 | The following example uses [SubCut](https://github.com/dickwall/subcut) for injecting the additional dependency:
173 |
174 | ```scala
175 | object SubCutDemo extends JFXApp {
176 |
177 | implicit val bindingModule = newBindingModule(module => {
178 | import module._
179 |
180 | bind [AnAdditionalDependency] toSingle(new AnAdditionalDependency("subcut dependency"))
181 | })
182 |
183 | stage = new JFXApp.PrimaryStage() {
184 | title = "Test window"
185 | scene = new Scene(
186 | FXMLView(
187 | getClass.getResource("test.fxml"),
188 | new SubCutDependencyResolver()))
189 | }
190 | }
191 | ```
192 |
193 | #### MacWire
194 |
195 | The following example demonstrates how to use [MacWire](https://github.com/adamw/macwire) to inject additional dependencies:
196 |
197 | ```scala
198 | object MacWireDemo extends JFXApp {
199 |
200 | class Module {
201 | def testDependency = TestDependency("MacWire dependency")
202 | }
203 |
204 | lazy val wired: Wired = wiredInModule(new Module)
205 |
206 | stage = new JFXApp.PrimaryStage() {
207 | title = "Hello world"
208 | scene = new Scene(
209 | FXMLView(getClass.getResource("startscreen.fxml"),
210 | new MacWireDependencyResolver(wired)))
211 | }
212 | }
213 | ```
214 |
215 | #### Guice
216 |
217 | The same example with [Guice](https://github.com/google/guice):
218 |
219 | ```scala
220 | object GuiceDemo extends JFXApp {
221 |
222 | val module = new AbstractModule {
223 | def configure() {
224 | bind(classOf[TestDependency]).toInstance(new TestDependency("guice dependency"))
225 | }
226 | }
227 | implicit val injector = Guice.createInjector(module)
228 |
229 | stage = new JFXApp.PrimaryStage() {
230 | title = "Hello world"
231 | scene = new Scene(
232 | FXMLView(getClass.getResource("startscreen.fxml"),
233 | new GuiceDependencyResolver()))
234 | }
235 | }
236 | ```
237 |
238 | ## Requirements
239 | * `sbt 0.13` is required
240 |
241 | ## Related
242 | * [Related blog post](https://vigoo.github.io/posts/2014-01-12-scalafx-with-fxml.html) explaining how the library works.
243 |
--------------------------------------------------------------------------------
/core-macros/src/main/scala/scalafxml/core/macros/sfxmlMacro.scala:
--------------------------------------------------------------------------------
1 | package scalafxml.core.macros
2 |
3 | import scala.language.experimental.macros
4 | import scala.annotation.StaticAnnotation
5 | import scala.reflect.macros.blackbox
6 |
7 | /** Annotates a class to generate a ScalaFXML controller around it
8 | *
9 | * == Overview ==
10 | * The annotated class will be moved to an inner class in the generated
11 | * proxy, named Controller. The proxy gets the annotated class' name,
12 | * and will have a constructor receiving a [[scalafxml.core.ControllerDependencyResolver]].
13 | * It implements the [[javafx.fxml.Initializable]] interface.
14 | *
15 | * The generated proxy has all the ScalaFX types from the original class'
16 | * constructor as public JavaFX variables annotated with the [[javafx.fxml.FXML]] attribute.
17 | *
18 | * All the public methods of the controller are copied to the proxy, delegating the call
19 | * to the inner controller, converting JavaFX event arguments to ScalaFX event arguments.
20 | *
21 | * The controller itself is instantiated in the proxy's initialize method.
22 | */
23 | class sfxml(additionalControls: List[String] = List.empty) extends StaticAnnotation {
24 | def macroTransform(annottees: Any*): Any = macro sfxmlMacro.impl
25 | }
26 |
27 | /** Annotates a controller constructor argument to treat it as a nested controller
28 | * @tparam Controller The nested controller's real (generated proxy) type (the argument type should be a trait implemented by
29 | * the inner controller)
30 | */
31 | class nested[Controller] extends StaticAnnotation {
32 |
33 | }
34 |
35 | class TypeCheckHelper[T] {
36 | }
37 |
38 | /** Macro transformation implementation */
39 | object sfxmlMacro {
40 |
41 | def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
42 |
43 | import c.universe._
44 |
45 | sealed trait InputType
46 | case class WrapWithScalaFX(jfxType: Tree, sfxType: Tree) extends InputType
47 | case class UseJavaFX(jfxType: Tree) extends InputType
48 | case class NestedController(controllerType: Tree, interfaceType: Tree) extends InputType
49 | case object GetFromDependencies extends InputType
50 |
51 | /** Resolves a type tree to a type */
52 | def toType(t: Tree): Type = {
53 | val e = c.Expr[Any](c.typecheck(q"new scalafxml.core.macros.TypeCheckHelper[$t]"))
54 | e.actualType match {
55 | case TypeRef(_, _, params) => params.head
56 | }
57 | }
58 |
59 | def evalTree[T](tree: Tree) = c.eval(c.Expr[T](c.untypecheck(tree.duplicate)))
60 |
61 | def additionalControls: List[String] = {
62 | c.prefix.tree match {
63 | case q"new sfxml(additionalControls = $additionalControls)" =>
64 | evalTree[List[String]](additionalControls)
65 | case q"new sfxml($additionalControls)" =>
66 | evalTree[List[String]](additionalControls)
67 | case q"new sfxml()" =>
68 | List.empty
69 | }
70 | }
71 |
72 | def isNestedAnnotation(t: Tree): Option[Tree] = {
73 | t match {
74 | case Apply(Select(New(AppliedTypeTree(Ident(TypeName("nested")), List(controllerType))), termNames.CONSTRUCTOR), List()) =>
75 | Some(controllerType)
76 | case _ =>
77 | None
78 | }
79 | }
80 |
81 | def isFxmlAnnotation(t: Tree): Boolean = {
82 | t match {
83 | case Apply(Select(New(Ident(TypeName("FXML"))), termNames.CONSTRUCTOR), List()) =>
84 | true
85 | case _ =>
86 | false
87 | }
88 | }
89 |
90 | /** Determines whether an input type of the annotated constructor
91 | * needs to be wrapped or fetched from the dependency provider
92 | *
93 | * @param t type tree possibly representing a ScalaFX type
94 | * @return returns one of the cases of the InputType ADT
95 | */
96 | def determineInputType(t: Tree, modifiers: Modifiers): InputType = {
97 | val unknownType = toType(t)
98 |
99 | val nestedControllerType: Option[Tree] = modifiers.annotations.map(isNestedAnnotation(_)).flatMap(_.toList).headOption
100 | nestedControllerType match {
101 | case Some(controllerType) => NestedController(controllerType, t)
102 | case None =>
103 |
104 | val name = unknownType.typeSymbol.name
105 | val pkg = unknownType.typeSymbol.owner
106 |
107 | val controlPrefixes = "javafx." :: additionalControls
108 |
109 | // We simply replace the package to javafx from scalafx,
110 | // and keep everything else
111 | if (pkg.isPackageClass) {
112 | val pkgName = pkg.fullName
113 | if (pkgName.startsWith("scalafx.")) {
114 |
115 | val args = unknownType.asInstanceOf[TypeRefApi].args
116 |
117 | val jfxPkgName = pkgName.replaceFirst("scalafx.", "javafx.")
118 | val jfxClassName = s"$jfxPkgName.$name"
119 | val jfxClass = c.mirror.staticClass(jfxClassName)
120 |
121 | WrapWithScalaFX(tq"$jfxClass[..$args]", t)
122 | } else if (controlPrefixes.exists(prefix => pkgName.startsWith(prefix))) {
123 | // If it is already a JavaFX type, we leave it as it is
124 | UseJavaFX(t)
125 | } else {
126 | if (modifiers.annotations.exists(isFxmlAnnotation(_))) {
127 | UseJavaFX(t)
128 | } else {
129 | GetFromDependencies
130 | }
131 | }
132 | } else {
133 | GetFromDependencies // default: no conversion
134 | }
135 | }
136 | }
137 |
138 | /** Converts a ScalaFX type tree to JavaFX type tree, or keep it untouched
139 | *
140 | * @param unknownType a type tree possibly representing a ScalaFX type
141 | * @return a type tree which is either modified to be a JavaFX type, or is untouched
142 | */
143 | def toJavaFXTypeOrOriginal(unknownType: Tree, modifiers: Modifiers): Tree =
144 | determineInputType(unknownType, modifiers) match {
145 | case WrapWithScalaFX(jfxType, _) => jfxType
146 | case UseJavaFX(jfxType) => jfxType
147 | case NestedController(controllerType, _) => controllerType
148 | case GetFromDependencies => unknownType
149 | }
150 |
151 | /** Filters out empty elements from a list of AST */
152 | def nonEmpty(ls: List[Option[Tree]]): List[Tree] =
153 | ls.flatMap(_.toList)
154 |
155 | // Extracting the name, constructor arguments, base class and body
156 | // from the annotated class
157 | val q"class $name(...$argss) extends $baseClass with ..$traits { ..$body }" = annottees.map(_.tree).head
158 |
159 | /** Bindable public JavaFX variables for the proxy,
160 | * generated from the constructor arguments of the controller
161 | * which have a ScalaFX type
162 | */
163 | val jfxVariables = nonEmpty(argss.flatten.map {
164 | case ValDef(paramModifiers, paramName, paramType, _) =>
165 | determineInputType(paramType, paramModifiers) match {
166 | case WrapWithScalaFX(jfxType, _) => Some(q"@javafx.fxml.FXML var $paramName: $jfxType = null")
167 | case UseJavaFX(jfxType) => Some(q"@javafx.fxml.FXML var $paramName: $jfxType = null")
168 | case NestedController(controllerType, _) => Some(q"@javafx.fxml.FXML var $paramName: $controllerType = null")
169 | case GetFromDependencies => None
170 | }
171 | case p =>
172 | throw new Exception(s"Unknown parameter match: ${showRaw(p)}")
173 | })
174 |
175 | /** Event handler delegates for the proxy, converting from JavaFX event argument types
176 | * to ScalaFX event argument types
177 | */
178 | val eventHandlers = nonEmpty(body.map {
179 | case DefDef(methodMods, methodName, _, methodParams, methodReturnType, _) if !methodMods.hasFlag(Flag.PRIVATE) =>
180 | val methodArgs = methodParams.map(_.map {
181 | case ValDef(pmods, pname, ptype, pdef) => ValDef(pmods, pname, toJavaFXTypeOrOriginal(ptype, pmods), pdef)
182 | })
183 | val argInstances = methodParams.map(_.map {
184 | case ValDef(pmods, pname, ptype, _) =>
185 | determineInputType(ptype, pmods) match {
186 | case WrapWithScalaFX(_, sfxType) => q"new $sfxType($pname)"
187 | case UseJavaFX(_) => q"$pname"
188 | case NestedController(_, interfaceType) => q"$pname.as[$interfaceType]()"
189 | case GetFromDependencies => q"$pname"
190 | }
191 | })
192 |
193 | Some(
194 | q"""@javafx.fxml.FXML def ${methodName.toTermName}(...$methodArgs) {
195 | impl.${methodName.toTermName}(...$argInstances)
196 | }
197 | """)
198 | case _ => None
199 | })
200 |
201 | /** List of values to be passed to the controller's constructor */
202 | val constructorParams = argss.map(_.map {
203 | case ValDef(cParamModifiers, cParamName, cParamType, _) =>
204 | determineInputType(cParamType, cParamModifiers) match {
205 | case WrapWithScalaFX(_, sfxType) => q"new $sfxType($cParamName)"
206 | case UseJavaFX(_) => q"$cParamName"
207 | case NestedController(_, interfaceType) => q"$cParamName.as[$interfaceType]()"
208 | case GetFromDependencies => q"getDependency[$cParamType](${Literal(Constant(cParamName.decodedName.toString))})"
209 | }
210 | })
211 |
212 | /** List of calls to the dependency resolver passed to the proxy as a constructor
213 | * argument, to get the controller's dependencies and store them through the
214 | * ProxyDependencyInjection trait.
215 | */
216 | val injections = nonEmpty(argss.flatten.map {
217 | case ValDef(cParamModifiers, cParamName, cParamType, _) =>
218 | determineInputType(cParamType, cParamModifiers) match {
219 | case GetFromDependencies =>
220 | val nameLiteral = Literal(Constant(cParamName.decodedName.toString))
221 | val typeLiteral = q"scala.reflect.runtime.universe.typeOf[$cParamType]"
222 | Some(
223 | q"""dependencyResolver.get($nameLiteral, $typeLiteral) match {
224 | case Some(value) => setDependency($nameLiteral, value)
225 | case None =>
226 | }
227 | """)
228 | case _ =>
229 | None
230 | }
231 | case x => throw new Exception(s"Invalid constructor argument $x")
232 | })
233 |
234 | /** AST of the proxy class */
235 | val proxyTree =
236 | q"""class $name(private val dependencyResolver: scalafxml.core.ControllerDependencyResolver) extends javafx.fxml.Initializable with scalafxml.core.FxmlProxyGenerator.ProxyDependencyInjection with scalafxml.core.ControllerAccessor {
237 |
238 | ..$injections
239 |
240 | class Controller(...$argss) extends $baseClass with ..$traits { ..$body }
241 | private var impl: Controller = null
242 |
243 | ..$jfxVariables
244 |
245 | def initialize(url: java.net.URL, rb: java.util.ResourceBundle) {
246 | impl = new Controller(...$constructorParams)
247 | }
248 |
249 | ..$eventHandlers
250 |
251 | def as[T](): T = impl.asInstanceOf[T]
252 |
253 | }"""
254 |
255 | // Returning the proxy class
256 | c.Expr[Any](proxyTree)
257 | }
258 | }
259 |
--------------------------------------------------------------------------------