TornadoFX Guide
User Manual: Pdf
Open the PDF directly: View PDF .
Page Count: 276 [warning: Documents this large are best viewed by clicking the View PDF Link!]
- Introduction
- 1. Why TornadoFX?
- 2. Setting Up
- 3. Components
- 4. Basic Controls
- 5. Data Controls
- 6. Type Safe CSS
- 7. Layouts and Menus
- 8. Charts
- 9. Shapes and Animation
- 10. FXML
- 11. Editing Models and Validation
- 12. OSGi
- 13. TornadoFX IDEA Plugin
- 14. Scopes
- 15. EventBus
- 16. Workspaces
- 17. Internationalization
- 18. Config Settings and State
- 19. JSON and REST
- 20. Dependency Injection
- 21. Wizard
- Appendix A - Supplementary Topics
- Appendix B - Tools and Utilities
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
1.9
1.10
1.11
1.12
1.13
1.14
1.15
1.16
1.17
1.18
1.19
1.20
1.21
1.22
1.23
1.24
TableofContents
Introduction
1.WhyTornadoFX?
2.SettingUp
3.Components
4.BasicControls
5.DataControls
6.TypeSafeCSS
7.LayoutsandMenus
8.Charts
9.ShapesandAnimation
10.FXML
11.EditingModelsandValidation
12.OSGi
13.TornadoFXIDEAPlugin
14.Scopes
15.EventBus
16.Workspaces
17.Internationalization
18.ConfigSettingsandState
19.JSONandREST
20.DependencyInjection
21.Wizard
AppendixA-SupplementaryTopics
AppendixB-ToolsandUtilities
1
Introduction
Userinterfacesarebecomingincreasinglycriticaltothesuccessofconsumerandbusiness
applications.Withtheriseofconsumermobileappsandwebapplications,businessusers
areincreasinglyholdingenterpriseapplicationstoahigherstandardofquality.Theywant
rich,feature-packeduserinterfacesthatprovideimmediateinsightandnavigatecomplex
screensintuitively.Moreimportantly,theywanttheapplicationtoadaptquicklytobusiness
changesonafrequentbasis.Forthedeveloper,thismeanstheapplicationmustnotonlybe
maintainablebutalsoevolvable.TornadoFXseekstoassistalltheseobjectivesandgreatly
streamlinethecodingofJavaFXUI's.
WhilemuchoftheenterpriseITworldispushingHTML5andcloud-basedapplications,
manybusinessesarestillusingdesktopUIframeworkslikeJavaFX.Whileitdoesnot
distributetolargeaudiencesaseasilyaswebapplications,JavaFXworkswellfor"in-house"
businessapplications.Itshigh-performancewithlargedatasets(andthefactitisnative
Java)makeitapracticalchoiceforapplicationsusedbehindthecorporatefirewall.
JavaFX,likemanyUIframeworks,canquicklybecomeverboseanddifficulttomaintain.
Fortunately,thereleaseofKotlinhascreatedanopportunitytorethinkhowJavaFX
applicationsarebuilt.
AninterestingproductthatisindevelopmentisJPro,aweb-basedJavaFXcontainer
thatusesnoplugins.ItcanworkwithTornadoFXandJavaFX,butisstillinclosedbeta
atthetimeofwriting.Youcanfollowtheprojectandwaitforitsavailabilityhere:
https://jpro.io/
WhyTornadoFX?
InFebruary2016,JetBrainsreleasedKotlin,anewJVMlanguagethatemphasizes
pragmatismoverconvention.Kotlinworksatahigherlevelofabstractionandprovides
practicallanguagefeaturesnotavailableinJava.Oneofthemoreimportantfeaturesof
Kotlinisits100%interoperabilitywithexistingJavalibrariesandcodebases,including
JavaFX.
WhileJavaFXcanbeusedwithKotlininthesamemannerasJava,somebelievedKotlin
hadlanguagefeaturesthatcouldstreamlineandsimplifyJavaFXdevelopment.Wellbefore
Kotlin'sbeta,EugenKissprototypedJavaFX"builders"withKotlinFX.InJanuary2016,
EdvinSyserebootedtheinitiativeandreleasedTornadoFX.
1.WhyTornadoFX?
3
TornadoFXseekstogreatlyminimizetheamountofcodeneededtobuildJavaFX
applications.Itnotonlyincludestype-safebuilderstoquicklylayoutcontrolsanduser
interfaces,butalsofeaturesdependencyinjection,delegatedproperties,controlextension
functions,andotherpracticalfeaturesenabledbyKotlin.TornadoFXisafineshowcaseof
howKotlincansimplifycodebases,andittacklestheverbosityofUIcodewitheleganceand
simplicity.ItcanworkinconjunctionwithotherpopularJavaFXlibrariessuchasControlsFX
andJFXtras.ItworksespeciallywellwithreactiveframeworkssuchasReactFXaswellas
RxJavaandfriends(includingRxJavaFX,RxKotlin,andRxKotlinFX).
ReaderRequirements
ThisbookexpectsreaderstohavesomeknowledgeofKotlinandhavespentsometime
gettingacquaintedwithit.TherewillbesomecoverageofKotlinlanguagefeaturesbutonly
toacertainextent.Ifyouhavenotdonesoalready,readtheJetBrainsKotlinReferenceand
spendagoodfewhoursstudyingit.
ItdefinitelyhelpstobefamiliarwithJavaFXbutitisnotarequirement.Perhapsyoustarted
studyingJavaFXbutfoundthedevelopmentexperiencetobetedious,andyouarechecking
outTornadoFXhopingitprovidesabetterwaytobuilduserinterfaces.Ifthisdescribesyour
experienceandyouarelearningKotlin,thenyouwillprobablybenefitfromthisguide.
AMotivationalExample
IfyouhaveworkedwithJavaFXbefore,youmighthavecreateda TableViewatsomepoint.
Sayyouhaveagivendomaintype Person.TornadoFXallowsyoutomuchmoreconcisely
createtheJavaBeans-likeconventionusedfortheJavaFXbinding.
classPerson(id:Int,name:String,birthday:LocalDate){
validProperty=SimpleIntegerProperty(id)
varidbyidProperty
valnameProperty=SimpleStringProperty(name)
varnamebynameProperty
valbirthdayProperty=SimpleObjectProperty(birthday)
varbirthdaybybirthdayProperty
valage:Intget()=Period.between(birthday,LocalDate.now()).years
}
Youcanthenbuildanentire" View"containinga TableViewwithasmallcodefootprint.
1.WhyTornadoFX?
4
classMyView:View(){
privatevalpersons=listOf(
Person(1,"SamanthaStuart",LocalDate.of(1981,12,4)),
Person(2,"TomMarks",LocalDate.of(2001,1,23)),
Person(3,"StuartGills",LocalDate.of(1989,5,23)),
Person(3,"NicoleWilliams",LocalDate.of(1998,8,11))
).observable()
overridevalroot=tableview(persons){
column("ID",Person::idProperty)
column("Name",Person::nameProperty)
column("Birthday",Person::birthdayProperty)
column("Age",Person::age)
}
}
RENDEREDOUTPUT:
Halfofthatcodewasjustinitializingsampledata!Ifyouhoneinonjustthepartdeclaringthe
TableViewwithfourcolumns(shownbelow),youwillseeittookasimplefunctional
constructtobuilda TableView.Itwillautomaticallysupporteditstothefieldsaswell.
tableview(persons){
column("ID",Person::idProperty)
column("Name",Person::nameProperty)
column("Birthday",Person::birthdayProperty)
column("Age",Person::age)
}
Asshownbelow,wecanusethe cellFormat()extensionfunctionona TableColumn,and
createconditionalformattingfor"Age"valuesthatarelessthan 18.
1.WhyTornadoFX?
5
tableview<Person>{
items=persons
column("ID",Person::idProperty)
column("Name",Person::nameProperty)
column("Birthday",Person::birthdayProperty)
column("Age",Person::age).cellFormat{
text=it.toString()
style{
if(it<18){
backgroundColor+=c("#8b0000")
textFill=Color.WHITE
}
}
}
}
RENDEREDOUTPUT:
ThesedeclarationsarepureKotlincode,andTornadoFXispackedwithexpressivepower
fordozensofcaseslikethis.Thisallowsyoutofocusoncreatingsolutionsratherthan
engineeringUIcode.YourJavaFXapplicationswillnotonlybeturnedaroundmorequickly,
butalsobemaintainableandevolvable.
1.WhyTornadoFX?
6
SettingUp
TouseTornadoFX,thereareseveraloptionstosetupthedependencyforyourproject.
MainstreambuildautomationtoolslikeGradleandMavenaresupportedandshouldhaveno
issuesingettingsetup.
PleasenotethatTornadoFXisaKotlinlibrary,andthereforeyourprojectneedstobe
configuredtouseKotlin.ForGradleandMavenconfigurations,pleaserefertotheKotlin
GradleSetupandKotlinMavenSetupguides.Makesureyourdevelopmentenvironmentor
IDEisequippedtoworkwithKotlinandhastheproperpluginsandcompilers.
ThisguidewilluseIntellijIDEAtowalkthroughcertainexamples.IDEAistheIDEofchoice
toworkwithKotlin,althoughEclipsehasapluginaswell.
Gradle
ForGradle,youcansetupthedependencydirectlyfromMavenCentral.Providethedesired
versionnumberforthe x.y.zplaceholder.
repositories{
mavenCentral()
}
//MinimumjvmTargetof1.8neededsinceKotlin1.1
compileKotlin{
kotlinOptions.jvmTarget=1.8
}
dependencies{
compile'no.tornado:tornadofx:x.y.z'
}
Maven
ToimportTornadoFXwithMaven,addthefollowingdependencytoyourPOMfile.Provide
thedesiredversionnumberforthe x.y.zplaceholder.
Goesintokotlin-maven-pluginblock:
2.SettingUp
7
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
Thenthisgoesinto dependenciesblock:
<dependency>
<groupId>no.tornado</groupId>
<artifactId>tornadofx</artifactId>
<version>x.y.z</version>
</dependency>
OtherBuildAutomationSolutions
ForinstructionsonhowtouseTornadoFXwithotherbuildautomationsolutions,pleaserefer
tothe[TornadoFXpageattheCentralRepository]
([http://search.maven.org/#search|gav|1|g%3A"no.tornado]
(http://search.maven.org/#search|gav|1|g%3A"no.tornado)"ANDa%3A"tornadofx")
ManualImport
TomanuallydownloadandimporttheJARfile,gototheTornadoFXreleasepageorthe
CentralRepository.DownloadtheJARfileandconfigureitintoyourproject.
StartingaTornadoFXApplication
NewerversionsoftheJVMknowhowtostartJavaFXapplicationswithouta main()
method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthat
extends javafx.application.Application.Since tornadofx.Appextends
javafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwould
starttheappbyreferencing com.example.app.MyApp,andyoudon'tnecessarilyneeda
main()functionunlessyouneedtosupplycommandlinearguments.Inthatcaseyou
wouldaddapackagelevelmainfunctiontothe MyApp.ktfile:
funmain(args:Array<String>){
Application.launch(MyApp::class.java,*args)
}
2.SettingUp
8
Thismainfunctionwouldbecompiledto com.example.app.MyAppKt-noticethe Ktatthe
end.Whenyoucreateapackage
levelmainfunction,itwillalwayshaveaclassnameofthefullyqualifiedpackage,plusthe
filename,appendedwith Kt.
Infact,TornadoFXalsocontainsahelpertomakeitevennicertolaunchapplicationsfroma
mainclassbyacceptingtheappclassasagenerictypeparameter:
funmain(args:Array<String>)=launch<MyApp>(args)
2.SettingUp
9
Components
JavaFXusesatheatricalanalogytoorganizean Applicationwith Stageand Scene
components.TornadoFXbuildsonthisbyproviding View, Controller,and Fragment
components.Whilethe Stage,and SceneareusedbyTornadoFX,the View,
Controller,and Fragmentintroducesnewconceptsthatstreamlinedevelopment.Manyof
thesecomponentsareautomaticallymaintainedassingletons,andcancommunicateto
eachotherthroughsimpledependencyinjectionsandothermeans.
YoualsohavetheoptiontoutilizeFXMLwhichwillbediscussedmuchlater.Butfirst,lets
extend ApptocreateanentrypointthatlaunchesaTornadoFXapplication.
AppandViewBasics
TocreateaTornadoFXapplication,youmusthaveatleastoneclassthatextends App.An
Appistheentrypointtotheapplicationandspecifiestheinitial View.Itdoesinfactextend
JavaFX Application,butyoudonotnecessarilyneedtospecifya start()or main()
method.
Butfirst,extend Apptocreateyourownimplementationandspecifytheprimaryviewasthe
firstconstructorargument.
classMyApp:App(MyView::class)
AViewcontainsdisplaylogicaswellasalayoutofNodes,similartotheJavaFX Stage.It
isautomaticallymanagedasasingleton.Whenyoudeclarea Viewyoumustspecifya
rootpropertywhichcanbeany Nodetype,andthatwillholdtheView'scontent.
InthesameKotlinfileorinanewfile,extendaclassoffof View.Overridetheabstract
rootpropertyandassignit VBoxoranyother Nodeyouchoose.
classMyView:View(){
overridevalroot=VBox()
}
However,wemightwanttopopulatethis VBoxactingasthe rootcontrol.Usingthe
initializerblock,let'saddaJavaFX Buttonanda Label.Youcanusethe"plusassign"
+=operatorstoaddchildren,suchasa Buttonand Label
3.Components
10
classMyView:View(){
overridevalroot=VBox()
init{
root+=Button("PressMe")
root+=Label("")
}
}
Whileitisprettyclearwhat'sgoingonfromlookingatthiscode,TornadoFXprovidesa
buildersyntaxthatwillstreamlineyourUIcodefurtherandmakeitmucheasiertoreason
abouttheresultingUIjustbylookingatthecode.Wewillgraduallymoveintobuildersyntax,
andfinallycoverbuildersinfullinthenextchapter.
Whileweintroduceyoutonewconcepts,youmightsometimesseecodethatisnotusing
bestpractices.Wedothistointroduceyougraduallytoconceptsandgiveyouabroader
understandingofwhatisgoingonunderthehood.Graduallywewillintroducemore
powerfulconstructstosolvetheproblemathandinabetterway.
Nextwewillseehowtorunthisapplication.
StartingaTornadoFXApplication
NewerversionsoftheJVMknowhowtostartJavaFXapplicationswithouta main()
method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthat
extends javafx.application.Application.Since tornadofx.Appextends
javafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwould
starttheappbyreferencing com.example.app.MyApp,andyoudonotnecessarilyneeda
main()functionunlessyouneedtosupplycommandlinearguments.Inthatcaseyou
wouldaddapackagelevelmainfunctiontothe MyApp.ktfile:
funmain(args:Array<String>){
Application.launch(MyApp::class.java,*args)
}
Thismainfunctionwouldbecompiledto com.example.app.MyAppKt.Noticethe Ktatthe
end.Whenyoucreateapackagelevelmainfunction,itwillalwayshaveaclassnameofthe
fullyqualifiedpackage,plusthefilename,appendedwith Kt.
Forlaunchingandtestingthe App,wewilluseIntellijIDEA.NavigatetoRun→Edit
Configurations(Figure3.1).
Figure3.1
3.Components
11
Clickthegreen"+"signandcreateanewApplicationconfiguration(Figure3.2).
Figure3.2
3.Components
12
Specifythenameofyour"Mainclass"whichshouldbeyour Appclass.Youwillalsoneed
tospecifythemoduleitresidesin.Givetheconfigurationameaningfulnamesuchas
"Launcher".Afterthatclick"OK"(Figure3.3).
Figure3.3
YoucanrunyourTornadoFXapplicationbyselectingRun→Run'Launcher'orwhateveryou
namedtheconfiguration(Figure3.4).
Figure3.4
3.Components
13
Youshouldnowseeyourapplicationlaunch(Figure3.5)
Figure3.5
Congratulations!Youhavewrittenyourfirst(albeitsimple)TornadoFXapplication.Itmaynot
looklikemuchrightnow,butaswecovermoreofTornadoFX'spowerfulfeatureswewillbe
creatinglarge,impressiveuserinterfaceswithlittlecodeinnotime.Butfirstlet'sunderstand
alittlebetterwhatishappeningbetween Appand View.
UnderstandingViews
Let'sdivealittledeeperintohowa Viewworksandhowitcanbeused.Takealookatthe
Appand Viewclasseswejustbuilt.
3.Components
14
classMyApp:App(MyView::class)
classMyView:View(){
overridevalroot=VBox()
init{
with(root){
this+=Button("PressMe")
this+=Label("Waiting")
}
}
}
A ViewcontainsahierarchyofJavaFXNodesandisinjectedbynamewhereveritiscalled.
Inthenextsectionwewilllearnhowtoleveragepowerfulbuilderstocreatethese Node
hierarchiesquickly.Thereisonlyoneinstanceof MyViewmaintainedbyTornadoFX,
effectivelymakingitasingleton.TornadoFXalsosupportsscopes,whichcangrouptogether
acollectionof Views, Fragmentsand Controllersinseparateinstances,resultingina
Viewonlybeingasingletoninsidethatscope.ThisisgreatforMultiple-DocumentInterface
applicationsandotheradvancedusecases.Thisiscoveredinalaterchapter.
Usinginject()andEmbeddingViews
YoucanalsoinjectoneormoreViewsintoanother View.Belowweembeda TopViewand
BottomViewintoa MasterView.Noteweusethe inject()delegatepropertytolazilyinject
the TopViewand BottomViewinstances.Thenwecalleach"child"View's roottoassign
themtothe BorderPane(Figure3.6).
classMasterView:View(){
valtopView:TopViewbyinject()
valbottomView:BottomViewbyinject()
overridevalroot=borderpane{
top=topView.root
bottom=bottomView.root
}
}
classTopView:View(){
overridevalroot=label("TopView")
}
classBottomView:View(){
overridevalroot=label("BottomView")
}
3.Components
15
Figure3.6
IfyouneedViewstocommunicatetoeachother,youcancreateapropertyineachofthe
"child"Viewsthatholdsthe"parent" View.
classMasterView:View(){
overridevalroot=BorderPane()
valtopView:TopViewbyinject()
valbottomView:BottomViewbyinject()
init{
with(root){
top=topView.root
bottom=bottomView.root
}
topView.parent=this
bottomView.parent=this
}
}
classTopView:View(){
overridevalroot=Label("TopView")
lateinitvarparent:MasterView
}
classBottomView:View(){
overridevalroot=Label("BottomView")
lateinitvarparent:MasterView
}
Moretypicallyyouwouldusea Controllerora ViewModeltocommunicatebetweenviews,
andwewillvisitthistopiclater.
InjectionUsingfind()
The inject()delegatewilllazilyassignagivencomponenttoaproperty.Thefirsttimethat
componentiscallediswhenitwillberetrieved.Alternatively,insteadofusingthe inject()
delegateyoucanusethe find()functiontoretrieveasingletoninstanceofa Viewor
othercomponents.
3.Components
16
classMasterView:View(){
overridevalroot=BorderPane()
valtopView=find(TopView::class)
valbottomView=find(BottomView::class)
init{
with(root){
top=topView.root
bottom=bottomView.root
}
}
}
classTopView:View(){
overridevalroot=Label("TopView")
}
classBottomView:View(){
overridevalroot=Label("BottomView")
}
Youcanuseeither find()or inject(),butusing inject()delegatesisthepreferred
meanstoperformdependencyinjection.
IntroductiontoBuilders
Whilewewillcoverbuildersmoreindepthinthenextchapter,itistimetorevealthatthe
aboveexamplecanbewritteninamuchmoreconciseandexpressivesyntax:
classMasterView:View(){
overridevalroot=borderpane{
top(TopView::class)
bottom(BottomView::class)
}
}
Insteadofinjectingthe TopViewand BottomViewandthenassigningtheirrespectiveroot
nodestothe BorderPanes topand bottomproperty,wespecifythe BorderPanewiththe
buildersyntax(alllowercase)andthendeclarativelytellTornadoFXtopullinthetwo
subviewsandassignthemtothe topand bottompropertiesautomatically.Hopefullyyou
agreethisismuchmoreexpressive,withalotlessboilerplate.Thisisoneofthemost
importantprinciplesTornadoFXtriestoliveby:Reduceboilerplateandincreasereadability.
Theendresultisoftenlesscodeandlessbugs.
3.Components
17
Controllers
Inmanycases,itisconsideredagoodpracticetoseparateaUIintothreedistinctparts:
1. Model-Thebusinesscodelayerthatholdscorelogicanddata
2. View-Thevisualdisplaywithvariousinputandoutputcontrols
3. Controller-The"middleman"mediatingeventsbetweentheModelandtheView
ThereareotherflavorsofMVClikeMVVMandMVP,allofwhichcanbeleveragedin
TornadoFX.
WhileyoucouldputalllogicfromtheModelandControllerrightintotheview,itisoften
cleanertoseparatethesethreepiecesdistinctlytomaximizereusability.Onecommonly
usedpatterntoaccomplishthisistheMVCpattern.InTornadoFX,a Controllercanbe
injectedtosupporta View.
Hereisasimpleexample.Createasimple Viewwitha TextFieldwhosevalueiswritten
toa"database"whena Buttonisclicked.Wecaninjecta Controllerthathandles
interactingwiththemodelthatwritestothedatabase.Sincethisexampleissimplified,there
willbenodatabasebutaprintedmessagewillserveasaplaceholder(Figure3.7).
classMyView:View(){
valcontroller:MyControllerbyinject()
varinputField:TextFieldbysingleAssign()
overridevalroot=vbox{
label("Input")
inputField=textfield()
button("Commit"){
action{
controller.writeToDb(inputField.text)
inputField.clear()
}
}
}
}
classMyController:Controller(){
funwriteToDb(inputValue:String){
println("Writing$inputValuetodatabase!")
}
}
Figure3.7
3.Components
18
WhenwebuildtheUI,wemakesuretoaddareferencetothe inputFieldsothatitcanbe
referencesfromthe onClickeventhandlerofthe"Commit"buttonlater.Whenthe
"Commit"buttonisclicked,youwillseetheControllerprintsalinetotheconsole.
WritingAlphatodatabase!
Itisimportanttonotethatwhiletheaboveworks,andmayevenlookprettygood,itisa
goodpracticetoavoidreferencingotherUIelementsdirectly.Yourcodewillbemucheasier
torefactorifyoubindyourUIelementstopropertiesandmanipulatethepropertiesinstead.
Wewillintroducethe ViewModellater,whichprovideseveneasierwaystodealwiththis
typeofinteraction.
YoucanalsouseControllerstoprovidedatatoa View(Figure3.8).
classMyView:View(){
valcontroller:MyControllerbyinject()
overridevalroot=vbox{
label("Myitems")
listview(controller.values)
}
}
classMyController:Controller(){
valvalues=FXCollections.observableArrayList("Alpha","Beta","Gamma","Delta")
}
Figure3.8
3.Components
19
The VBoxcontainsa Labelanda ListView,andthe itemspropertyofthe ListViewis
assignedtothe valuespropertyofour Controller.
Whethertheyarereadingorwritingdata,Controllerscanhavelong-runningtasksand
shouldnotperformworkontheJavaFXthread.Youwilllearnhowtoeasilyoffloadworktoa
workerthreadusingthe runAsyncconstructlaterinthischapter.
Longrunningtasks
Wheneveryoucallafunctioninacontrolleryouneedtodetermineifthatfunctionreturns
immediatelyorifitperformspotentiallylong-runningtasks.Ifyoucallafunctiononthe
JavaFXApplicationThread,theUIwillbeunresponsiveuntilthecallcompletes.
UnresponsiveUI'sisakillerforuseracceptance,somakesurethatyourunexpensive
operationsinthebackground.TornadoFXprovidesthe runAsyncfunctiontohelpwiththis.
Codeplacedinsidea runAsyncblockwillruninthebackground.Iftheresultofthe
backgroundcallshouldupdateyourUI,youmustmakesurethatyouapplythechangeson
theJavaFXApplicationThread.The uiblockdoesexactlythat.
3.Components
20
valtextfield=textfield()
button("Updatetext"){
action{
runAsync{
myController.loadText()
}ui{loadedText->
textfield.text=loadedText
}
}
}
Whenthebuttonisclicked,theactioninsidethe actionbuilder(whichdelegatesthe
ActionEventto setActionmethod)isrun.Itmakesacalloutto
myController.loadText()andappliestheresulttothetextpropertyofthetextfieldwhenit
returns.TheUIstaysresponsivewhilethecontrollerfunctionruns.
Underthecovers, runAsynccreatesJavaFX Taskobjects,andspinsoffaseparatethread
torunyourcallinsidethe Task.Youcanassignthis TasktoavariableandbindittoaUI
toshowprogresswhileyouroperationisrunning.
Infact,thisissocommonthatthereisalsoandefaultViewModelcalled TaskStatuswhich
containsobservablevaluesfor running, message, title,and progress.Youcansupply
the runAsynccallwithaspecificinstanceofthe TaskStatusobject,orusethedefault.
TheTornadoFXsourcesincludesanexampleusageofthisinthe AsyncProgressApp.ktfile.
Thereisalsoaversionof runAsynccalled runAsyncWithProgresswhichwillcoverthe
currentnodewithaprogressindicatorwhilethelongrunningoperationruns.
singleAssign()PropertyDelegate
Intheexampleaboveweinitializedthe inputFieldpropertywiththe singleAssign
delegate.Ifyouwanttoguaranteethatavalueisonlyassignedonce,youcanusethe
singleAssign()delegateinsteadofthe lateinitkeywordfromKotlin.Thiswillcausea
secondassignmenttothrowanerror,anditwillalsoerrorwhenitisprematurelyaccessed
beforeitisassigned.
Youcanlookupmoreabout singleAssign()indetailinAppendixA1,butknowfornowit
guaranteesa varcanonlybeassignedonce.Itisalsothreadsafeandhelpsmitigate
issueswithmutability.
Fragment
3.Components
21
Any Viewyoucreateisasingleton,whichmeansyoutypicallyuseitinonlyoneplaceata
time.Thereasonforthisisthattherootnodeofthe Viewcanonlyhaveasingleparentina
JavaFXapplication.Ifyouassignitanotherparent,itwilldisappearfromitspreviousparent.
However,ifyouwouldliketocreateapieceofUIthatisshort-livedorcanbeusedin
multipleplaces,considerusinga Fragment.AFragmentisaspecialtypeof Viewthatcan
havemultipleinstances.TheyareparticularlyusefulforpopupsoraspiecesofalargerUI
(suchasListCells,whichwelookatviathe ListCellFragmentlater).
Both Viewand Fragmentsupport openModal(), openWindow()and openInternalWindow()
thatwillopentherootnodeinaseparateWindow.
classMyView:View(){
overridevalroot=vbox{
button("PressMe"){
action{
find(MyFragment::class).openModal(stageStyle=StageStyle.UTILITY)
}
}
}
}
classMyFragment:Fragment(){
overridevalroot=label("Thisisapopup")
}
Youcanpassoptionalargumentsto openModal()aswelltomodifyafewofitsbehaviors.
OptionalArgumentsforopenModal()
Argument Type Description
stageStyle StageStyle Definesoneofthepossibleenumstylesfor
Stage.Default: StageStyle.DECORATED
modality Modality
Definesoneofthepossibleenummodality
typesfor Stage.Default:
Modality.APPLICATION_MODAL
escapeClosesWindow Boolean Setsthe ESCkeytocall closeModal().
Default: true
owner Window SpecifytheownerWindowforthisStage`
block Boolean BlockUIexecutionuntiltheWindowcloses.
Default: false
InternalWindow
3.Components
22
While openModalopensinanew Stage, openInternalWindowopensoverthecurrentroot
node,oranyothernodeifyouspecifyit:
button("Openeditor"){
action{
openInternalWindow(Editor::class)
}
}
Figure3.9
AgoodusecasefortheinternalwindowisforsinglestageenvironmentslikeJPro,orifyou
wanttocustomizethewindowtrimtomakethewindowappearmoreinlinewiththedesign
ofyourapplication.TheInternalWindowcanbestyledwithCSS.Takealookatthe
InternalWindow.Stylesclassformoreinformationaboutstyleableproperties.
TheinternalwindowAPIdiffersfrommodal/windowinoneimportantaspect.Sincethe
windowopensoveranexistingnode,youtypicallycall openInternalWindow()fromwithinthe
Viewyouwantittoopenontopof.YousupplytheViewyouwanttoshow,andyoucan
optionallysupplywhatnodetoopenoverviathe ownerparameter.
OptionalArgumentsforopenInternalWindow()
3.Components
23
Argument Type Description
view UIComponent Thecomponentwillbethecontentofthe
newwindow
view KClass Alternatively,youcansupplytheclassofthe
viewinsteadofaninstance
icon Node Optionalwindowicon
scope Scope Ifyouspecifytheviewclass,youcanalso
specifythescopeusedtofetchtheview
modal Boolean
Definesifthecoveringnodeshouldbe
disabledwhiletheinternalwindowisactive.
Default: true
escapeClosesWindow Boolean Setsthe ESCkeytocall close().Default:
true
owner Node
SpecifytheownerNodeforthiswindow.The
windowwillbydefaultcovertherootnodeof
thisview.`
Closingmodalwindows
Any Componentopenedusing openModal(), openWindow()or openInternalWindow()canbe
closedbycalling closeModal().Itisalsopossibletogettothe InternalWindowinstance
directlyifneededusing findParentOfType(InternalWindow::class).
ReplacingViewsandDockingEvents
WithTornadoFX,iseasytoswapyourcurrent Viewwithanother Viewusing
replaceWith(),andoptionallyaddatransition.Intheexamplebelow,a Buttononeach
Viewwillswitchtotheotherview,whichcanbe MyView1or MyView2(Figure3.10).
3.Components
24
classMyView1:View(){
overridevalroot=vbox{
button("GotoMyView2"){
action{
replaceWith(MyView2::class)
}
}
}
}
classMyView2:View(){
overridevalroot=vbox{
button("GotoMyView1"){
action{
replaceWith(MyView1::class)
}
}
}
}
Figure3.10
Youalsohavetheoptiontospecifyaspiffyanimationforthetransitionbetweenthetwo
Views.
replaceWith(MyView1::class,ViewTransition.Slide(0.3.seconds,Direction.LEFT)
Thisworksbyreplacingthe rootNodeongiven Viewwithanother View's root.There
aretwofunctionsyoucanoverrideon ViewtoleveragewhenaView's rootNodeis
connectedtoaparent( onDock()),andwhenitisdisconnected( onUndock()).Youcan
leveragethesetwoeventstoconnectand"cleanup"whenevera Viewcomesinorfalls
out.Youwillnoticerunningthecodebelowthatwhenevera Viewisswapped,itwillundock
thatprevious Viewanddockthenewone.Youcanleveragethesetwoeventstomanage
initializationanddisposaltasks.
3.Components
25
classMyView1:View(){
overridevalroot=vbox{
button("GotoMyView2"){
action{
replaceWith(MyView2::class)
}
}
}
overridefunonDock(){
println("DockingMyView1!")
}
overridefunonUndock(){
println("UndockingMyView1!")
}
}
classMyView2:View(){
overridevalroot=vbox{
button("GotoMyView1"){
action{
replaceWith(MyView1::class)
}
}
}
overridefunonDock(){
println("DockingMyView2!")
}
overridefunonUndock(){
println("UndockingMyView2!")
}
}
Passingparameterstoviews
ThebestwaytopassinformationbetweenviewsisoftenaninjectedViewModel.Evenso,it
canstillbeconvenienttobeabletopassparameterstoothercomponents.The findand
injectfunctionssupportsvarargsof Pair<String,Any>whichcanbeusedforjustthis
purpose.Consideracustomerlistthatopensacustomereditorfortheselectedcustomer.
Theactiontoeditacustomermightlooklikethis:
funeditCustomer(customer:Customer){
find<CustomerEditor>(mapOf(CustomerEditor::customertocustomer).openWindow())
}
3.Components
26
Theparametersarepassedasamap,wherethekeyisthepropertyintheviewandthe
valueiswhateveryouwantthepropertytobe.Thisgivesyouatypesafewayofconfiguring
parametersforthetargetView.
HereweusetheKotlin tosyntaxtocreatetheparameter.Thiscouldalsohavebeen
writtenas Pair(CustomerEditor::customer,customer)ifyouprefer.Theeditorcannow
accesstheparameterlikethis:
classCustomerEditor:Fragment(){
valcustomer:Customerbyparam()
}
Ifyouwanttoinspecttheparametersinsteadofblindlyrelyingonthemtobeavailable,you
caneitherdeclarethemasnullableorconsultthe paramsmap:
classCustomerEditor:Fragment(){
init{
valcustomer=params["customer"]as?Customer
if(customer!=null){
...
}
}
}
Ifyoudon'tcareabouttypesafetyyoucanalsopassparametersas mapOf("customer"to
customer),butthenyoumissoutonautomaticrefactoringifyourenameapropertyinthe
targetview.
Accessingtheprimarystage
Viewhasapropertycalled primaryStagethatallowsyoutomanipulatepropertiesofthe
Stagebackingit,suchaswindowsizeforexample.Any Viewor Fragmentthatwere
openedvia openModalwillalsohavea modalStagepropertyavailable.
Accessingthescene
Sometimesitisnecessarytogetaholdofthecurrentscenefromwithina Viewor
Fragment.Thiscanbeachievedwith root.scene,orifyouarewithinatypesafebuilder,
thereisanevenshorterway,justuse scene.
3.Components
27
Accessingresources
Lot'sofJavaFXAPIstakesresourcesasan URLorthe toExternalFormofanURL.To
retrievearesourceurlonewouldtypicallywritesomethinglike:
valmyAudioClip=AudioClip(MyView::class.java.getResource("mysound.wav").toExternalFo
rm())
Every Componenthasa resourcesobjectwhichcanretrievetheexternalformurlofa
resourcelikethis:
valmyAudiClip=AudioClip(resources["mysound.wav"])
Ifyouneedanactual URLitcanberetrievedlikethis:
valmyResourceURL=resources.url("mysound.wav")
The resourceshelperalsohasseveralotherhelpfulfunctionstohelpyouturnfilesrelative
tothe Componentintoanobjectofthetypeyouneed:
valmyJsonObject=resources.json("myobject.json")
valmyJsonArray=resources.jsonArray("myarray.json")
valmyStream=resources.stream("somefile")
It'sworthmentioningthatthe jsonand jsonArrayfunctionsarealsoavailableon
InputStreamobjects.
Resourcesarerelativetothe Componentbutyoucanalsoretrievearesourcebyit'sfull
path,startingwitha /.
Shortcutsandkeycombinationsforactions
Youcanfireactionswhencertainkeycombinationsaretyped.Thisisdonewiththe
shortcutfunction:
shortcut(KeyCombination.valueOf("Ctrl+Y")){
doSomething()
}
3.Components
28
Thereisalsoastringversionofthe shortcutfunctionthatdoesthesamebutisless
verbose:
shortcut("Ctrl+Y")){
doSomething()
}
Youcanalsoaddshortcutstobuttonactionsdirectly:
button("Save"){
action{doSave()}
shortcut("Ctrl+S")
}
TouchSupport
JavaFXsupportstouchoutofthebox,andfornowtheonlyplaceweneededtoimproveit
wastohandleshortpressandlongpressinamoreconvenientway.Itconsistsoftwo
functionssimilarto action,whichcanbeconfiguredonany Node:
shortpress{println("Activatedonshortpress")}
longpress{println("Activatedonlongpress")}
Bothfunctionsacceptsa consumeparameterwhichbydefaultis false.Settingittotrue
willpreventeventbubblingforthepressevent.The longpressfunctionadditionally
supportsa thresholdparameterwhichisusedtodeterminewhenalongpresshasaccured.
Itis 700.millisbydefault.
Summary
TornadoFXisfilledwithsimple,streamlined,andpowerfulinjectiontoolstomanageViews
andControllers.ItalsostreamlinesdialogsandothersmallUIpiecesusing Fragment.While
theapplicationswebuiltsofarareprettysimple,hopefullyyouappreciatethesimplified
conceptsTornadoFXintroducestoJavaFX.Inthenextchapterwewillcoverwhatis
arguablythemostpowerfulfeatureofTornadoFX:Type-SafeBuilders.
3.Components
29
BasicControls
OneofthemostexcitingfeaturesofTornadoFXaretheType-SafeBuilders.Configuringand
layingoutcontrolsforcomplexUI'scanbeverboseanddifficult,andthecodecanquickly
becomemessytomaintain.Fortunately,youcanuseapowerfulclosurepatternpioneered
byGroovytocreatestructuredUIlayoutswithpureandsimpleKotlincode.
WhilewewilllearnhowtoapplyFXMLlater,youmayfindbuilderstobeanexpressive,
robustwaytocreatecomplexUI'sinafractionofthetime.Therearenoconfigurationfilesor
compilermagictricks,andbuildersaredonewithpureKotlincode.Thenextseveral
chapterswilldividethebuildersintoseparatecategoriesofcontrols.Alongtheway,youwill
graduallybuildmorecomplexUI'sbyintegratingthesebuilderstogether.
Butfirst,let'scoverhowbuildersactuallywork.
HowBuildersWork
Kotlin'sstandardlibrarycomeswithahandfulofhelpful"block"functionstotargetitemsof
anytype T.Thereisthewith()function,whichallowsyoutowritecodeagainstanitemasif
youwererightinsideofitsclass.
classMyView:View(){
overridevalroot=VBox()
init{
with(root){
this+=Button("PressMe")
}
}
}
Intheaboveexample,the with()functionacceptsthe rootasanargument.The
followingclosureargumentmanipulates rootdirectlybyreferringtoitas this,whichis
safelyinterpretedasa VBox.A Buttonwasaddedtothe VBoxbycallingit's
plusAssign()extendedoperator.
Alternatively,everytypeinKotlinhasanapply()function.Thisisalmostthesame
functionalityas with()butitisactuallyanextendedhigher-orderfunction.
4.BasicControls
30
classMyView:View(){
overridevalroot=VBox()
init{
root.apply{
this+=Button("PressMe")
}
}
}
Both with()and apply()accomplishasimilartask.Theysafelyinterpretthetypetheyare
targetingandallowmanipulationstobedonetoit.However, with()returnsthelast
statementwithinthelambda,whereas apply()doesinfactreturntheitemitwastargeting.
Therefore,ifyoucall apply()ona Buttontomanipulatesay,itsfontcolorandaction,itis
helpfulthe Buttonreturnsitselfsoastonotbreakthedeclarationflow.
classMyView:View(){
overridevalroot=VBox()
init{
with(root){
this+=Button("PressMe").apply{
textFill=Color.RED
action{println("Buttonpressed!")}
}
}
}
}
Thebasicconceptsofhowbuildersworkareexpressedabove,andtherearethreetasks
beingdone:
1. A Buttoniscreated
2. The Buttonismodified
3. The Buttonisaddedtoits"parent",whichisa VBox
Whendeclaringany Node,thesethreestepsaresocommonthatTornadoFXstreamlines
themforyouusingstrategicallyplacedextensionfunctions,suchas button()asshown
below.
4.BasicControls
31
classMyView:View(){
overridevalroot=VBox()
init{
with(root){
button("PressMe"){
textFill=Color.RED
action{println("Buttonpressed!")}
}
}
}
}
Whilethislooksmuchcleaner,youmightbewondering:"Howdidwejustgetridofthe this
+=and apply()functioncall?Andwhyareweusingafunctioncalled button()insteadof
anactual Button?"
Wewillnotgotoodeeponhowthisisdone,andyoucanalwaysdigintothesourcecodeif
youarecurious.
Butessentially,the VBox(oranytargetablecomponent)hasanextensionfunctioncalled
button().
Itacceptsatextargumentandanoptionalclosuretargetinga Buttonitwillinstantiate.
Whenthisfunctioniscalled,itwillcreatea Buttonwiththespecifiedtext,applytheclosure
toit,addittothe VBoxitwascalledon,andthenreturnit.
Takingthisefficiencyfurther,youcanoverridethe rootina View,butassignitabuilder
functionandavoidneedingany initand with()blocks.
classMyView:View(){
overridevalroot=vbox{
button("PressMe"){
textFill=Color.RED
action{println("Buttonpressed!")}
}
}
}
Thebuilderpatternbecomesespeciallypowerfulwhenyoustartnestingcontrolsintoother
controls.Usingthesebuilderextensionfunctions,youcaneasilypopulateandnestmultiple
HBoxinstancesintoa VBox,andcreateUIcodethatisclearlystructured(Figure4.1).
4.BasicControls
32
classMyView:View(){
overridevalroot=vbox{
hbox{
label("FirstName")
textfield()
}
hbox{
label("LastName")
textfield()
}
button("LOGIN"){
useMaxWidth=true
}
}
}
Figure4.1
AlsonotewewilllearnaboutTornadoFX'sproprietary Formlater,whichwillmake
simpleinputUI'slikethisevensimplertocode.
IfyouneedtosavereferencestocontrolssuchastheTextFields,youcansavethemto
variablesorpropertiessincethefunctionsreturntheproducedcontrols.Itisrecommendyou
usethe singleAssign()delegatestoensurethepropertiesareonlyassignedonce.
4.BasicControls
33
classMyView:View(){
varfirstNameField:TextFieldbysingleAssign()
varlastNameField:TextFieldbysingleAssign()
overridevalroot=vbox{
hbox{
label("FirstName")
firstNameField=textfield()
}
hbox{
label("LastName")
lastNameField=textfield()
}
button("LOGIN"){
useMaxWidth=true
action{
println("Logginginas${firstNameField.text}${lastNameField.text}")
}
}
}
}
Notethatnon-builderextensionfunctionsandpropertieshavebeenaddedtodifferent
controlsaswell.The useMaxWidthisanextendedpropertyfor Node,anditsetsthe Node
tooccupythemaximumwidthallowed.Wewillseemoreofthesehelpfulextensions
throughoutthenextfewchapters.
Inthecomingchapters,wewillcovereachcorrespondingbuilderforeachJavaFXcontrol.
Withtheconceptsunderstoodabove,youcanreadaboutthesenextchaptersstarttofinish
orasareference.
BuildersforBasicControls
TherestofthischapterwillcoverbuildersforcommonJavaFXcontrolslike Button,
Label,and TextField.Thenextchapterwillcoverbuildersfordata-drivencontrolslike
ListView, TableView,and TreeTableView.
Button
Forany Pane,youcancallits button()extensionfunctiontoadda Buttontoit.Youcan
optionallypassa textargumentanda Button.()->Unitlambdatomodifyitsproperties.
4.BasicControls
34
Withina Pane,thiswilladda Buttonwithredtextandprint"Buttonpressed!"everytimeit
isclicked(Figure4.2)
button("PressMe"){
textFill=Color.RED
action{
println("Buttonpressed!")
}
}
Figure4.2
Label
Youcancallthe label()extensionfunctiontoadda Labeltoagiven Pane.
Optionallyyoucanprovideatext(oftype Stringor Property<String>),agraphic
(oftype Nodeor ObjectProperty<Node>)anda Label.()->Unitlambdatomodifyits
properties(Figure4.3).
label("Loremipsum",circle(10,10,5)){
textFill=Color.BLUE
}
Figure4.3
TextField
Forany Paneyoucanadda TextFieldbycallingits textfield()extensionfunction
(Figure4.4).
textfield()
Figure4.4
4.BasicControls
35
Youcanoptionallyprovideinitialtextaswellasaclosuretomanipulatethe TextField.For
example,wecanaddalistenertoits textProperty()andprintitsvalueeverytimeit
changes(Figure4.5).
textfield("Inputsomething"){
textProperty().addListener{obs,old,new->
println("Youtyped:"+new)
}
}
Figure4.6
PasswordField
Ifyouneeda TextFieldtotakesensitiveinformation,youmightwanttoconsidera
PasswordFieldinstead.Itwillshowanonymouscharacterstoprotectfrompryingeyes.You
canalsoprovideaninitialpasswordasanargumentandablocktomanipulateit(Figure
4.7).
passwordfield("my_password"){
requestFocus()
}
Figure4.7
CheckBox
Youcancreatea CheckBoxtoquicklycreateatrue/falsestatecontrolandoptionally
manipulateitwithablock(Figure4.8).
4.BasicControls
36
checkbox("AdminMode"){
action{println(isSelected)}
}
Noticethattheactionblockiswrappedinsidethecheckboxsoyoucanaccess
it's isSelectedproperty.Ifyoudon'tneedaccesstothepropertiesoftheCheckBoxyou
couldhavewritten checkbox("AdminMode").action{}.
Figure4.9
Youcanalsoprovidea Property<Boolean>thatwillbindtoitsselectionstate.
valbooleanProperty=SimpleBooleanProperty()
checkbox("AdminMode",booleanProperty).action{println(isSelected)}
ComboBox
A ComboBoxisadropdowncontrolthatallowsafixedsetofvaluestobeselectedfrom
(Figure4.10).
valtexasCities=FXCollections.observableArrayList("Austin",
"Dallas","Midland","SanAntonio","FortWorth")
combobox<String>{
items=texasCities
}
Figure4.10
4.BasicControls
37
Youdonotneedtospecifythegenerictypeifyoudeclarethe valuesasanargument.
valtexasCities=FXCollections.observableArrayList("Austin",
"Dallas","Midland","SanAntonio","FortWorth")
combobox(values=texasCities)
Youcanalsospecifya Property<T>tobeboundtotheselectedvalue.
valtexasCities=FXCollections.observableArrayList("Austin",
"Dallas","Midland","SanAntonio","FortWorth")
valselectedCity=SimpleStringProperty()
combobox(selectedCity,texasCities)
ToggleButton
A ToggleButtonisabuttonthatexpressesatrue/falsestatedependingonitsselectionstate
(Figure4.11).
togglebutton("OFF"){
action{
text=if(isSelected)"ON"else"OFF"
}
}
4.BasicControls
38
PerhapsamoreidomaticwaytocontrolthebuttontextwouldbetouseaStringBinding
boundtothe textProperty:
togglebutton{
valstateText=selectedProperty().stringBinding{
if(it==true)"ON"else"OFF"
}
textProperty().bind(stateText)
}
Figure4.11
Youcanoptionallypassa ToggleGrouptothe togglebutton()function.Thiswillensureall
ToggleButtonsinthat ToggleGroupcanonlyhaveoneselectedatatime(Figure4.12).
classMyView:View(){
privatevaltoggleGroup=ToggleGroup()
overridevalroot=hbox{
togglebutton("YES",toggleGroup)
togglebutton("NO",toggleGroup)
togglebutton("MAYBE",toggleGroup)
}
}
Figure4.12
RadioButton
A RadioButtonisthesamefunctionalityasa ToggleButtonbutwithadifferentvisualstyle.
Whenitisselected,it"fills"inacircularcontrol(Figure4.13).
radiobutton("PowerUserMode"){
action{
println("PowerUserMode:$isSelected")
}
}
4.BasicControls
39
Figure4.13
Alsolikethe ToggleButton,youcanseta RadioButtontobeincludedina ToggleGroupso
thatonlyoneiteminthatgroupcanbeselectedatatime(Figure4.14).
classMyView:View(){
privatevaltoggleGroup=ToggleGroup()
overridevalroot=vbox{
radiobutton("Employee",toggleGroup)
radiobutton("Contractor",toggleGroup)
radiobutton("Intern",toggleGroup)
}
}
Figure4.14
DatePicker
The DatePickerisasimpletodeclare.Itallowsyoutochooseadatefromapopout
calendarcontrol.Youcanoptionallyprovideablocktomanipulateit(Figure4.15).
datepicker{
value=LocalDate.now()
}
Figure4.15
4.BasicControls
40
Youcanalsoprovidea Property<LocalDate>asanargumenttobindtoitsvalue.
valdateProperty=SimpleObjectProperty<LocalDate>()
datepicker(dateProperty){
value=LocalDate.now()
}
TextArea
The TextAreaallowsyouinputmultilinefreeformtext.Youcanoptionallyprovidetheinitial
text valueaswellasablocktomanipulateitondeclaration(Figure4.16).
textarea("Typememohere"){
selectAll()
}
Figure4.16
4.BasicControls
41
ProgressBar
A ProgressBarvisualizesprogresstowardscompletionofaprocess.Youcanoptionally
provideaninitial Doublevaluelessthanorequalto1.0indicatingpercentageofcompletion
(Figure4.17).
progressbar(0.5)
Figure4.17
Hereisamoredynamicexamplesimulatingprogressoverashortperiodoftime.
progressbar(){
thread{
for(iin1..100){
Platform.runLater{progress=i.toDouble()/100.0}
Thread.sleep(100)
}
}
}
Youcanalsopassa Property<Double>thatwillbindthe progresstoitsvalueaswellasa
blocktomanipulatethe ProgressBar.
4.BasicControls
42
progressbar(completion){
progressProperty().addListener{
obsVal,old,new->print("VALUE:$new")
}
}
ProgressIndicator
A ProgressIndicatorisfunctionallyidenticaltoa ProgressBarbutusesafillingcircle
insteadofabar(Figure4.18).
progressindicator{
thread{
for(iin1..100){
Platform.runLater{progress=i.toDouble()/100.0}
Thread.sleep(100)
}
}
}
Figure4.18
Justlikethe ProgressBaryoucanprovidea Property<Double>and/orablockasoptional
arguments(Figure4.19).
valcompletion=SimpleObjectProperty(0.0)
progressindicator(completion)
ImageView
Youcanembedanimageusing imageview().
imageview("tornado.jpg")
Figure4.19
4.BasicControls
43
Likemostothercontrols,youcanuseablocktomodifyitsattributes(Figure4.20).
imageview("tornado.jpg"){
scaleX=.50
scaleY=.50
}
Figure4.20
4.BasicControls
44
ScrollPane
Youcanembedacontrolinsidea ScrollPanetomakeitscrollable.Whentheavailablearea
becomessmallerthanthecontrol,scrollbarswillappeartonavigatethecontrol'sarea.
Forinstance,youcanwrapan ImageViewinsidea ScrollPane(Figure4.21).
scrollpane{
imageview("tornado.jpg")
}
Figure4.21
4.BasicControls
45
Keepinmindthatmanycontrolslike TableViewand TreeTableViewalreadyhavescroll
barsonthem,sowrappingthemina ScrollPaneisnotnecessary(Figure4.22).
Hyperlink
Youcancreatea Hyperlinkcontroltomimicthebehaviorofatypicalhyperlinktoafile,a
website,orsimplyperformanaction.
hyperlink("OpenFile").action{println("Openingfile...")}
Figure4.22
Text
Youcanaddasimplepieceof Textwithformattedproperties.Thiscontrolissimplerand
rawerthana Label,andparagraphscanbeseparatedusing \ncharacters(Figure4.23).
4.BasicControls
46
text("Veni\nVidi\nVici"){
fill=Color.PURPLE
font=Font(20.0)
}
Figure4.23
TextFlow
Ifyouneedtoconcatenatemultiplepiecesoftextwithdifferentformats,the TextFlow
controlcanbehelpful(Figure4.24).
textflow{
text("Tornado"){
fill=Color.PURPLE
font=Font(20.0)
}
text("FX"){
fill=Color.ORANGE
font=Font(28.0)
}
}
Figure4.24
Youcanaddany Nodetothe textflow,includingimages,usingthestandardbuilder
functions.
Tooltips
Insideany Nodeyoucanspecifya Tooltipviathe tooltip()function(Figure4.25).
4.BasicControls
47
button("Commit"){
tooltip("Writesinputtothedatabase")
}
Figure4.25
Likemostotherbuilders,youcanprovideaclosuretocustomizethe Tooltipitself.
button("Commit"){
tooltip("Writesinputtothedatabase"){
font=Font.font("Verdana")
}
}
Therearemanyotherbuildercontrols,andthemaintainersofTornadoFXhavestrived
tocreateabuilderforeveryJavaFXcontrol.Ifyouneedsomethingthatisnotcovered
here,useGoogletoseeifitsincludedinJavaFX.Chancesareifacontrolisavailable
inJavaFX,thereisabuilderwiththesamenameinTornadoFX.
SUMMARY
InthischapterwelearnedaboutTornadoFXbuildersandhowtheyworksimplybyusing
Kotlinextensionfunctions.Wealsocoveredbuildersforbasiccontrolslike Button,
TextFieldand ImageView.Inthecomingchapterswewilllearnaboutbuildersfortables,
layouts,menus,charts,andothercontrols.Asyouwillsee,combiningallthesebuilders
togethercreatesapowerfulwaytoexpresscomplexUI'swithverystructuredandminimal
code.
ThesearenottheonlycontrolbuildersintheTornadoFXAPI,andthisguidedoesitsbestto
keepup.AlwayschecktheTornadoFXGitHubtoseethelatestbuildersandfunctionalities
available,andfileanissueifyouseeanymissing.
4.BasicControls
48
4.BasicControls
49
DataControls
Anysignificantapplicationworkswithdata,andprovidingameansforuserstoview,
manipulate,andmodifydataisnotatrivialtaskforuserinterfacedevelopment.Fortunately,
TornadoFXstreamlinesmanyJavaFXdatacontrolssuchas ListView, TableView,
TreeView,and TreeTableView.Thesecontrolscanbecumbersometosetupinapurely
object-orientedway.Butusingbuildersthroughfunctionaldeclarations,wecancodeall
thesecontrolsinamuchmorestreamlinedway.
ListView
A ListViewissimilartoa ComboBoxbutitdisplaysallitemswithina ScrollViewandhas
theoptionofallowingmultipleselections,asshowninFigure5.1
listview<String>{
items.add("Alpha")
items.add("Beta")
items.add("Gamma")
items.add("Delta")
items.add("Epsilon")
selectionModel.selectionMode=SelectionMode.MULTIPLE
}
Figure5.1
Youcanalsoprovideitan ObservableListofitemsupfrontandomitthetypedeclaration
sinceitcanbeinferred.UsinganObservableListalsohasthebenefitthatchangestothelist
willautomaticallybereflectedintheListView.
5.DataControls
50
valgreekLetters=listOf("Alpha","Beta",
"Gamma","Delta","Epsilon").observable()
listview(greekLetters){
selectionModel.selectionMode=SelectionMode.MULTIPLE
}
Likemostdatacontrols,keepinmindthatbydefault,the ListViewwillcall toString()to
renderthetextforeachiteminyourdomainclass.Torenderanythingelse,youwillneedto
createyourowncustomcellformatting.
CustomCellFormattinginListView
Eventhoughthedefaultlookofa ListViewisratherboring(becauseitcalls toString()
andrendersitastext)youcanmodifyitsothateverycellisacustom Nodeofyour
choosing.Bycalling cellCache(),TornadoFXprovidesaconvenientwaytooverridewhat
kindof Nodeisreturnedforeachiteminyourlist(Figure5.2).
5.DataControls
51
classMyView:View(){
valpersons=listOf(
Person("JohnMarlow",LocalDate.of(1982,11,2)),
Person("SamanthaJames",LocalDate.of(1973,2,4))
).observable()
overridevalroot=listview(persons){
cellFormat{
graphic=cache{
form{
fieldset{
field("Name"){
label(it.name)
}
field("Birthday"){
label(it.birthday.toString())
}
label("${it.age}yearsold"){
alignment=Pos.CENTER_RIGHT
style{
fontSize=22.px
fontWeight=FontWeight.BOLD
}
}
}
}
}
}
}
}
classPerson(valname:String,valbirthday:LocalDate){
valage:Intget()=Period.between(birthday,LocalDate.now()).years
}
5.DataControls
52
Figure5.2-Acustomcellrenderingfor ListView
The cellFormatfunctionletsyouconfigurethe textand/or graphicpropertyofthecell
wheneveritcomesintoviewonthescreen.Thecellsthemselvesarereused,butwhenever
the ListViewasksthecelltoupdateit'scontent,the cellFormatfunctioniscalled.Inour
exampleweonlyassignto graphic,butifyoujustwanttochangethestringrepresentation
youshouldassignitto text.Itiscompletelylegitimatetoassignittoboth textand
graphic.Thevalueswillautomaticallybeclearedbythe cellFormatfunctionwhena
certainlistcellisnotshowinganactiveitem.
Notethatassigningnewnodestothe graphicpropertyeverytimethelistcellisaskedto
updatecanbeexpensive.Itmightbefineformanyusecases,butforheavynodegraphs,or
nodegraphswhereyouutilizebindingtowardstheuicomponentsinsidethecell,youshould
cachetheresultingnodesothenodegraphwillonlybecreatedoncepernode.Thisisdone
usingthe cachewrapperintheaboveexample.
5.DataControls
53
AssignIfNull
Ifyouhaveareasonforwantingtorecreatethegraphicpropertyforalistcell,youcanuse
the assignIfNullhelper,whichwillassignavaluetoanygivenpropertyiftheproperty
doesn'talreadycontainavalue.Thiswillmakesurethatyouavoidcreatingnewnodesif
updateItemiscalledonacellthatalreadyhasagraphicpropertyassigned.
cellFormat{
graphicProperty().assignIfNull{
label("Hello")
}
}
ListCellFragment
The ListCellFragmentisaspecialfragmentwhichcanhelpyoumanage ListViewcells.It
extends Fragment,andincludessomeextra ListViewspecificfieldsandhelpers.You
neverinstantiatethesefragmentsmanually,insteadyouinstructthe ListViewtocreate
themasneeded.Thereisaonetoonecorrelationbetween ListCelland
ListCellFragmentinstances.One ListCellFragmentinstancewilloveritslifecyclebeused
torepresentdifferentitems.
Tounderstandhowthisworks,let'sconsideramanuallyimplemented ListCell,essentially
thewayyouwoulddoinvanillaJavaFX.The updateItemfunctionwillbecalledwhenthe
ListCellshouldrepresentanewitem,noitem,orjustanupdatetothesameitem.When
youusea ListCellFragment,youdonotneedtoimplementsomethingakinto updateItem,
butthe itemPropertyinsideitwillupdatetorepresentthenewitemautomatically.Youcan
listentochangestothe itemProperty,orbetteryet,binditdirectlytoa ViewModel.That
wayyourUIcanbinddirectlytothe ViewModelandnolongerneedtocareaboutchangesto
theunderlyingitem.
Let'srecreatetheformfromthe cellFormatexampleusinga ListCellFragment.Weneeda
ViewModelwhichwewillcall PersonModel(Pleaseseethe EditingModelsandValidation
chapterforafullexplanationofthe ViewModel)Fornow,justimaginethatthe ViewModel
actsasaproxyforanunderlying Person,andthatthe Personcanbechangedwhilethe
observablevaluesinthe ViewModelremainthesame.Whenwehavecreatedour
PersonCellFragment,weneedtoconfigurethe ListViewtouseit:
listview(personlist){
cellFragment(PersonCellFragment::class)
}
5.DataControls
54
Nowcomesthe ListCellFragmentitself.
classPersonListFragment:ListCellFragment<Person>(){
valperson=PersonModel().bindTo(this)
overridevalroot=form{
fieldset{
field("Name"){
label(person.name)
}
field("Birthday"){
label(person.birthday)
}
label(stringBinding(person.age){"$valueyearsold"}){
alignment=Pos.CENTER_RIGHT
style{
fontSize=22.px
fontWeight=FontWeight.BOLD
}
}
}
}
}
BecausethisFragmentwillbereusedtorepresentdifferentlistitems,theeasiestapproach
istobindtheuielementstotheViewModel'sproperties.
The nameand birthdaypropertiesarebounddirectlytothelabelsinsidethefields.The
agestringinthelastlabelneedstobeconstructedusinga stringBindingtomakesureit
updateswhentheitemchanges.
Whilethismightseemlikeslightlymoreworkthanthe cellFormatexample,thisapproach
makesitpossibletoleverageeverythingtheFragmentclasshastooffer.Italsoforcesyou
todefinethecellnodegraphoutsideofthebuilderhierarchy,whichimprovesrefactoring
possibilitiesandenablescodereuse.
Additionalhelpersandeditingsupport
The ListCellFragmentalsohavesomeotherhelperproperties.Theyincludethe
cellPropertywhichwillupdatewhenevertheunderlyingcellchangesandthe
editingProperty,whichwilltellyouifthistheunderlyinglistcellisineditingmode.There
arealsoeditinghelperfunctionscalled startEdit, commitEdit, cancelEditplusan
onEditcallback.The ListCellFragmentmakesittrivialtoutilizetheexistingediting
capabilitesofthe ListView.AcompleteexamplecanbeseenintheTodoMVCdemo
application.
5.DataControls
55
TableView
ProbablyoneofthemostsignificantbuildersinTornadoFXistheonefor TableView.Ifyou
haveworkedwithJavaFX,youmighthaveexperiencedbuildinga TableViewinanobject-
orientedway.ButTornadoFXprovidesafunctionaldeclarationconstructpatternusing
extensionfunctionsthatgreatlysimplifiesthecodingofa TableView.
Sayyouhaveadomaintype,suchas Person.
classPerson(valid:Int,valname:String,valbirthday:LocalDate){
valage:Intget()=Period.between(birthday,LocalDate.now()).years
}
Takeseveralinstancesof Personandputtheminan ObservableList.
privatevalpersons=listOf(
Person(1,"SamanthaStuart",LocalDate.of(1981,12,4)),
Person(2,"TomMarks",LocalDate.of(2001,1,23)),
Person(3,"StuartGills",LocalDate.of(1989,5,23)),
Person(3,"NicoleWilliams",LocalDate.of(1998,8,11))
).observable()
Youcanquicklydeclarea TableViewwithallofitscolumnsusingafunctionalconstruct,and
specifythe itemspropertytoan ObservableList<Person>(Figure5.3).
tableview(persons){
column("ID",Person::id)
column("Name",Person::name)
column("Birthday",Person::birthday)
column("Age",Person::age)
}
Figure5.3
5.DataControls
56
The column()functionsareextensionfunctionsfor TableViewacceptinga headername
andamappedpropertyusingreflectionsyntax.TornadoFXwillthentakeeachmappingto
renderavalueforeachcellinthatgivencolumn.
Ifyouwantgranularcontrolover TableViewcolumnresizepolicies,seeAppendixA2
formoreinformationon SmartResizepolicies.
Using"Property"properties
IfyoufollowtheJavaFX Propertyconventionstosetupyourdomainclass,itwill
automaticallysupportvalueediting.
Youcancreatethese Propertyobjectstheconventionalway,oryoucanuseTornadoFX's
propertydelegatestoautomaticallycreatethese Propertydeclarationsasshownbelow.
classPerson(id:Int,name:String,birthday:LocalDate){
varidbyproperty(id)
funidProperty()=getProperty(Person::id)
varnamebyproperty(name)
funnameProperty()=getProperty(Person::name)
varbirthdaybyproperty(birthday)
funbirthdayProperty()=getProperty(Person::birthday)
valage:Intget()=Period.between(birthday,LocalDate.now()).years
}
Youneedtocreate xxxProperty()functionsforeachpropertytosupportJavaFX'snaming
conventionwhenitusesreflection.Thiscaneasilybedonebyrelayingtheircallsto
getProperty()toretrievethe Propertyforagivenfield.SeeAppendixA1fordetailed
5.DataControls
57
informationonhowthesepropertydelegateswork.
Nowonthe TableView,youcanmakeiteditable,maptotheproperties,andapplythe
appropriatecell-editingfactoriestomakethevalueseditable.
overridevalroot=tableview(persons){
isEditable=true
column("ID",Person::idProperty).useTextField(IntegerStringConverter())
column("Name",Person::nameProperty).useTextField(DefaultStringConverter())
column("Birthday",Person::birthdayProperty).useTextField(LocalDateStringConverter
())
column("Age",Person::age)
}
Toalloweditingandrendering,TornadoFXprovidesafewdefaultcellfactoriesyoucan
invokeonacolumneasilythroughextensionfunctions.
Extension
Function Description
useTextField() Usesastandard TextFieldtoeditvalueswithaprovided
StringConverter
useComboBox() Editsacellvalueviaa ComboBoxwithaspecified
ObservableList<T>ofapplicablevalues
useChoiceBox() Acceptsvaluechangestoacellwitha ChoiceBox
useCheckBox() Rendersaneditable CheckBoxfora Booleanvaluecolumn
useProgressBar() Rendersthecellasa ProgressBarfora Doublevaluecolumn
PropertySyntaxAlternatives
Ifyoudonotcareaboutexposingthe Propertyinafunction(whichiscommoninpractial
usage)youcanexpressyourclasslikethis:
classPerson(id:Int,name:String,birthday:LocalDate){
validProperty=SimpleIntegerProperty(id)
varidbyidProperty
valnameProperty=SimpleStringProperty(name)
varnamebynameProperty
valbirthdayProperty=SimpleObjectProperty(birthday)
varbirthdaybybirthdayProperty
valage:Intget()=Period.between(birthday,LocalDate.now()).years
}
5.DataControls
58
Thisalternativepatternexposesthe Propertyasafieldmemberinsteadofafunction.If
youliketheabovesyntaxbutwanttokeepthefunction,youcanmaketheproperty
privateandaddthefunctionlikethis:
privatevalnameProperty=SimpleStringProperty(name)
funnameProperty()=nameProperty
varnamebynameProperty
Choosingfromthesepatternsareallamatteroftaste,andyoucanusewhateverversion
meetsyourneedsorpreferencesbest.
YoucanalsoconvertplainpropertiestoJavaFXpropertiesusingtheTornadoFXPlugin.
RefertoChapter13tolearnhowtodothis.
UsingcellFormat()
Thereareotherextensionfunctionsappliedto TableViewthatcanassisttheflowof
declaringa TableView.Forinstance,youcancalla cellFormat()functiononagiven
columntoapplyformattingrules,suchashighlighting"Age"valueslessthan18(Figure5.4).
tableview(persons){
column("ID",Person::id)
column("Name",Person::name)
column("Birthday",Person::birthday)
column("Age",Person::age).cellFormat{
text=it.toString()
style{
if(it<18){
backgroundColor+=c("#8b0000")
textFill=Color.WHITE
}else{
backgroundColor+=Color.WHITE
textFill=Color.BLACK
}
}
}
}
Figure5.4
5.DataControls
59
Accessingnestedproperties
Let'sassumeour Personobjecthasa parentpropertywhichisalsoofoftype Person.To
createacolumnfortheparentname,wehaveseveraloptions.Ourfirstattemptissimply
extractingthenamepropertymanually:
column<Person,String>("Parentname",{it.value.parentProperty.value.nameProperty})
Noticehowwecan'tsimplyreferencetheproperty,weneedtoaccessthevalueprovidedin
thecallbacktogettotheactualinstanceandnestfromtheredowntothenameProperty.
Whilethisworks,ithasonemajordrawback.Iftheparentchanges,thelistwon'tbe
updated.Wecanpartiallyremedythisbydefiningthevalueforthepropertyastheparent
itself,andformattingit'sname:
column("Parentname",Person::parentProperty).cellFormat{
textProperty().bind(it.parentProperty.value.nameProperty)
}
Itmightstillnotupdaterightaway,eventhoughitwouldeventuallybecomeconsistentasthe
TableViewrefreshes.
Tocreateabindingthatwouldreflectachangetotheparentpropertyimmediately,consider
usingaselectbinding:(moreonbindingslater)
column<Person,String>("Parentname",{it.value.parentProperty.select(Person::namePro
perty)})
5.DataControls
60
DeclaringColumnValuesFunctionally
Ifyouneedtomapacolumn'svaluetoanon-property(suchasafunction),youcanusea
non-reflectionmeanstoextractthevaluesforthatcolumn.
Sayyouhavea WeeklyReporttypethathasa getTotal()functionacceptinga DayOfWeek
argument(anenumofMonday,Tuesday...Sunday).
abstractclassWeeklyReport(valstartDate:LocalDate){
abstractfungetTotal(dayOfWeek:DayOfWeek):BigDecimal
}
Let'ssayyouwantedtocreateacolumnforeach DayOfWeek.Youcannotmaptoproperties,
butyoucanmapeach WeeklyReportitemexplicitlytoextracteachvalueforthat
DayOfWeek.
tableview<WeeklyReport>{
for(dayOfWeekinDayOfWeek.values()){
column<WeeklyReport,BigDecimal>(dayOfWeek.toString()){
ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))
}
}
}
Thismorecloselyresemblesthetraditional setCellValueFactory()fortheJavaFX
TableColumn.
RowExpanders
Laterwewilllearnaboutthe TreeTableViewwhichhasanotionof"parent"and"child"rows,
buttheconstraintwiththiscontrolistheparentandchildmusthavethesamecolumns.
Fortunately,TornadoFXcomeswithanawesomeutilitytonotonlyreveala"childtable"fora
givenrow,butanykindof Nodecontrol.
Saywehavetwodomaintypes: Regionand Branch.A Regionisageographicalzone,
anditcontainsoneormore Branchitemswhicharespecificbusinessoperationlocations
(warehouses,distributioncenters,etc).Hereisadeclarationofthesetypesandsomegiven
instances.
5.DataControls
61
classRegion(valid:Int,valname:String,valcountry:String,valbranches:Observa
bleList<Branch>)
classBranch(valid:Int,valfacilityCode:String,valcity:String,valstateProvince
:String)
valregions=listOf(
Region(1,"PacificNorthwest","USA",listOf(
Branch(1,"D","Seattle","WA"),
Branch(2,"W","Portland","OR")
).observable()),
Region(2,"Alberta","Canada",listOf(
Branch(3,"W","Calgary","AB")
).observable()),
Region(3,"Midwest","USA",listOf(
Branch(4,"D","Chicago","IL"),
Branch(5,"D","Frankfort","KY"),
Branch(6,"W","Indianapolis","IN")
).observable())
).observable()
Wecancreatea TableViewwhereeachrowhasa rowExpander()functiondefined,and
therewecanarbitrarilycreateany Nodecontrolbuiltoffthatparticularrow'sitem.Inthis
case,wecannestanother TableViewforagiven Regiontoshowallthe Branchitems
belongingtoit.Itwillhavea"+"buttoncolumntoexpandandshowthisexpandedcontrol
(Figure5.5).
Figure5.5
5.DataControls
62
Thereareafewconfigurabilityoptions,like"expandondouble-click"behaviorsand
accessingthe expanderColumn(thecolumnwiththe"+"button)todriveapadding(Figure
5.6).
overridevalroot=tableview(regions){
column("ID",Region::id)
column("Name",Region::name)
column("Country",Region::country)
rowExpander(expandOnDoubleClick=true){
paddingLeft=expanderColumn.width
tableview(it.branches){
column("ID",Branch::id)
column("FacilityCode",Branch::facilityCode)
column("City",Branch::city)
column("State/Province",Branch::stateProvince)
}
}
}
Figure5.6
5.DataControls
63
The rowExpander()functiondoesnothavetoreturna TableViewbutanykindof Node,
includingFormsandothersimpleorcomplexcontrols.
Accessingtheexpandercolumn
Youmightwanttomanipulateorcallfunctionsontheactualexpandercolumn.Ifyou
activateexpandondoubleclick,youmightnotwanttoshowtheexpandercolumninthe
tableatall.Firstweneedareferencetotheexpander:
valexpander=rowExpander(true){...}
Ifyouwanttohidetheexpandercolumn,justcall expander.isVisible=false.Youcanalso
programmaticallytoggletheexpandedstateofanycolumnbycalling
expander.toggleExpanded(rowIndex).
TreeView
The TreeViewcontainselementswhereeachelementmaycontainchildelements.Typically
arrowsallowyoutoexpandaparentelementtoseeitschildren.Forinstance,wecannest
employeesunderdepartmentnames
5.DataControls
64
TraditionallyinJavaFX,populatingtheseelementsisrathercumbersomeandverbose.
FortunatelyTornadoFXmakesitrelativelysimple.
Sayyouhaveasimpletype Personandan ObservableListcontainingseveralinstances.
dataclassPerson(valname:String,valdepartment:String)
valpersons=listOf(
Person("MaryHanes","Marketing"),
Person("SteveFolley","CustomerService"),
Person("JohnRamsy","ITHelpDesk"),
Person("ErlickFoyes","CustomerService"),
Person("ErinJames","Marketing"),
Person("JacobMays","ITHelpDesk"),
Person("LarryCable","CustomerService")
)
Creatinga TreeViewwiththe treeview()buildercanbedonefunctionallyFigure5.7).
//CreatePersonobjectsforthedepartments
//withthedepartmentnameasPerson.name
valdepartments=persons
.map{it.department}
.distinct().map{Person(it,"")}
treeview<Person>{
//Createrootitem
root=TreeItem(Person("Departments",""))
//MakesurethetextineachTreeItemisthenameofthePerson
cellFormat{text=it.name}
//Generateitems.Childrenoftherootitemwillcontaindepartments
populate{parent->
if(parent==root)departmentselsepersons.filter{it.department==parent.
value.name}
}
}
Figure5.7
5.DataControls
65
Let'sbreakthisdown:
valdepartments=persons
.map{it.department}
.distinct().map{Person(it,"")}
Firstwegatheradistinctlistofallthe departmentsderivedfromthe personslist.Butthen
weputeach departmentStringina Personobjectsincethe TreeViewonlyaccepts
Personelements.Whilethisisnotveryintuitive,thisistheconstraintanddesignof
TreeView.Wemustmakeeach departmenta Personforittobeaccepted.
treeview<Person>{
//Createrootitem
root=TreeItem(Person("Departments",""))
5.DataControls
66
Nextwespecifythehighest rootforthe TreeViewthatalldepartmentswillbenested
under,andwegiveitaplaceholder Personcalled"Departments".
cellFormat{text=it.name}
Thenwespecifythe cellFormat()torenderthe nameofeach Person(including
departments)oneachcell.
populate{parent->
if(parent==root)departmentselsepersons.filter{it.department==parent.
value.name}
}
Finally,wecallthe populate()functionandprovideablockinstructinghowtoprovide
childrentoeach parent.Ifthe parentisindeedthe root,thenwereturnthe
departments.Otherwisethe parentisa departmentandweprovidealistof Person
objectsbelongingtothat department.
DatadrivenTreeView
Ifthechildlistyoureturnfrom populateisan ObservableList,anychangestothatlistwill
automaticallybereflectedintheTreeView.Thepopulatefunctionwillbecalledforanynew
childrenthatappears,andremoveditemswillresultinremovedTreeItemsaswell.
TreeViewwithDifferingTypes
Itisnotnecessarilyintuitivetomakeeveryentityinthepreviousexamplea Person.We
madeeachdepartmenta Personaswellasthe root"Departments".Foramorecomplex
TreeView<T>where Tisunknownandcanbeanynumberoftypes,itisbettertoleverage
starprojectionfortype T.
Usingstarprojection,youcansafelypopulatemultipletypesnestedintothe TreeView.
Forinstance,youcancreatea Departmenttypeandleverage cellFormat()toutilizetype-
checkingforrendering.Thenyoucanusea populate()functionthatwilliterateovereach
element,andyouspecifythechildrenforeachelement(ifany).
5.DataControls
67
dataclassDepartment(valname:String)
//CreateDepartmentobjectsforthedepartmentsbygettingdistinctvaluesfromPerso
n.department
valdepartments=persons.map{it.department}.distinct().map{Department(it)}
//TypesafewayofextractingthecorrectTreeItemtext
cellFormat{
text=when(it){
isString->it
isDepartment->it.name
isPerson->it.name
else->throwIllegalArgumentException("Invalidvaluetype")
}
}
//Generateitems.Childrenoftherootitemwillcontaindepartments,childrenofdep
artmentsarefiltered
populate{parent->
valvalue=parent.value
if(parent==root)departments
elseif(valueisDepartment)persons.filter{it.department==value.name}
elsenull
}
TreeTableView
The TreeTableViewoperatesandfunctionssimilarlytoa TreeView,butithasmultiple
columnssinceitisatable.Pleasenotethatthecolumnsina TreeTableViewarethesame
foreachparentandchildelement.Ifyouwantthecolumnstobedifferentbetweenparent
andchild,usea TableViewwitha rowExpander()ascoveredearlierinthischapter.
Sayyouhavea Personclassthatoptionallyhasan employeesparameter,whichdefaults
toanempty List<Person>ifnobodyreportstothat Person.
classPerson(valname:String,
valdepartment:String,
valemail:String,
valemployees:List<Person>=emptyList())
Thenyouhavean ObservableList<Person>holdinginstancesofthisclass.
5.DataControls
68
valpersons=listOf(
Person("MaryHanes","ITAdministration","mary.hanes@contoso.com",listOf(
Person("JacobMays","ITHelpDesk","jacob.mays@contoso.com"),
Person("JohnRamsy","ITHelpDesk","john.ramsy@contoso.com"))),
Person("ErinJames","HumanResources","erin.james@contoso.com",listOf(
Person("ErlickFoyes","CustomerService","erlick.foyes@contoso.com"),
Person("SteveFolley","CustomerService","steve.folley@contoso.com"),
Person("LarryCable","CustomerService","larry.cable@contoso.com")))
).observable()
Youcancreatea TreeTableViewbymergingthecomponentsneededfora TableViewand
TreeViewtogether.Youwillneedtocallthe populate()functionaswellassettheroot
TreeItem.
valtreeTableView=TreeTableView<Person>().apply{
column("Name",Person::nameProperty)
column("Department",Person::departmentProperty)
column("Email",Person::emailProperty)
///Createtherootitemthatholdsalltoplevelemployees
root=TreeItem(Person("Employeesbyleader","","",persons))
//Alwaysreturnemployeesunderthecurrentperson
populate{it.value.employees}
//Expandthetwofirstlevels
root.isExpanded=true
root.children.forEach{it.isExpanded=true}
//Resizetodisplayallelementsonthefirsttwolevels
resizeColumnsToFitContent()
}
Itisalsopossibletoworkwithmoreofanadhocbackingstorelikea Map.Thatwouldlook
somethinglikethis:
5.DataControls
69
valtableData=mapOf(
"Fruit"toarrayOf("apple","pear","Banana"),
"Veggies"toarrayOf("beans","cauliflower","cale"),
"Meat"toarrayOf("poultry","pork","beef")
)
treetableview<String>(TreeItem("Items")){
column<String,String>("Type",{it.value.valueProperty()})
populate{
if(it.value=="Items")tableData.keys
elsetableData[it.value]?.asList()
}
}
DataGrid
A DataGridissimilartothe GridPaneinthatitdisplaysitemsinaflexiblegridofrowsand
columns,butthesimilaritiesendsthere.Whilethe GridPanerequiresyoutoaddNodesto
thechildrenlist,the DataGridisdatadriveninthesamewayas TableViewand ListView.
Yousupplyitwithalistofitemsandtellithowtoconvertthosechildrentoagraphical
representation.
Itsupportsselectionofeitherasingleitemormultipleitemsatatimesoitcanbeusedasfor
examplethedisplayofanimageviewerorothercomponentswhereyouwantavisual
representationoftheunderlyingdata.Usagewiseitisclosetoa ListView,butyoucan
createanarbitraryscenegraphinsideeachcellsoitiseasytovisualizemultipleproperties
foreachitem.
valkittens=listOf("http://i.imgur.com/DuFZ6PQb.jpg","http://i.imgur.com/o2QoeNnb.j
pg")//moreitemshere
datagrid(kittens){
cellCache{
imageview(it)
}
}
Figure5.8
5.DataControls
70
The cellCachefunctionreceiveseachiteminthelist,andsinceweusedalistofStringsin
ourexample,wesimplypassthatstringtothe imageview()buildertocreatean ImageView
insideeachtablecell.Itisimportanttocallthe cellCachefunctioninsteadofthe
cellFormatfunctiontoavoidrecreatingtheimageseverytimethe DataGridredraws.Itwill
reusetheitems.
Let'screateascenegraphthatisalittlebitmoreinvolved,andalsochangethedefaultsize
ofeachcell:
5.DataControls
71
valnumbers=(1..10).toList()
datagrid(numbers){
cellHeight=75.0
cellWidth=75.0
multiSelect=true
cellCache{
stackpane{
circle(radius=25.0){
fill=Color.FORESTGREEN
}
label(it.toString())
}
}
}
Figure5.9
Thegridissuppliedwithalistofnumbersthistime.Westartbyspecifyingacellheightand
widthof75pixels,halfofthedefaultsize.Wealsoconfiguremultiselecttobeabletoselect
morethanasingleelement.Thisisashortcutofwriting selectionModel.selectionMode=
SelectionMode.MULTIPLEviaanextensionproperty.Wecreatea StackPanethatstacksa
Labelontopofa Circle.
5.DataControls
72
Youmightwonderwhythelabelgotsobigandboldbydefault.Thisiscomingfromthe
defaultstylesheet.Thestylesheetisagoodstartingpointforfurthercustomization.All
propertiesofthedatagridcanbeconfiguredincodeaswellasinCSS,andthe
stylesheetlistsallpossiblestyleproperties.
Thenumberlistshowcasedmultipleselection.Whenacellisselected,itreceivesthe
CSSpseudoclassof selected.Bydefaultitwillbehavemostlylikea ListViewrow
withregardstoselectionstyles.Youcanaccessthe selectionModelofthedatagridto
listenforselectionchanges,seewhatitemsareselectedetc.
Summary
Functionalconstructsworkwellwithdatacontrolslike TableView, TreeView,andotherswe
haveseeninthischapter.Usingthebuilderpatterns,youcanquicklyandfunctionally
declarehowdataisdisplayed.
InChapter7,wewillembedcontrolsinlayoutstocreatemorecomplexUI'seasily.
5.DataControls
73
Type-SafeCSS
WhileyoucancreateplaintextCSSstylesheetsinJavaFX,TornadoFXprovidestheoption
tobringtype-safetyandcompiledCSStoJavaFX.Youcanconvenientlychoosetocreate
stylesinitsownclass,ordoitinlinewithinacontroldeclaration.
InlineCSS
Thequickestandeasiestwaytostyleacontrolontheflyistocallagiven Node'sinline
style{}function.AlltheCSSpropertiesavailableonagivencontrolareavailableina
type-safemanner,withcompilationchecksandauto-completion.
Forexample,youcanstylethebordersona Button(usingthe box()function),boldits
font,androtateit(Figure6.1).
button("PressMe"){
style{
fontWeight=FontWeight.EXTRA_BOLD
borderColor+=box(
top=Color.RED,
right=Color.DARKGREEN,
left=Color.ORANGE,
bottom=Color.PURPLE
)
rotate=45.deg
}
setOnAction{println("Youpressedthebutton")}
}
Figure6.1
Thisisespeciallyhelpfulwhenyouwanttostyleacontrolwithoutbreakingthedeclaration
flowofthe Button.However,keepinmindthe style{}willreplaceallstylesappliedto
thatcontrolunlessyoupass trueforitsoptional appendargument.
6.TypeSafeCSS
74
style(append=true){
....
}
Sometimesyouwanttoapplythesamestylestomanynodesinonego.The style{}
functioncanalsobeappliedtoanyIterablethatcontainsNodes:
vbox{
label("First")
label("Second")
label("Third")
children.style{
fontWeight=FontWeight.BOLD
}
}
The fontWeightstyleisappliedtoallchildrenofthevbox,inessenceallthelabelswe
added.
Whenyourstylingcomplexitypassesacertainthreshold,youmaywanttoconsiderusing
Stylesheetswhichwewillcovernext.
ApplyingStyleClasseswithStylesheets
Ifyouwanttoorganize,re-use,combine,andoverridestylesyouneedtoleveragea
Stylesheet.TraditionallyinJavaFX,astylesheetisdefinedinaplainCSStextfileincluded
intheproject.However,TornadoFXallowscreatingstylesheetswithpureKotlincode.This
hasthebenefitsofcompilationchecks,auto-completion,andotherperksthatcomewith
staticallytypedcode.
Todeclarea Stylesheet,extenditontoyourownclasstoholdyourcustomizedstyles.
importtornadofx.*
classMyStyle:Stylesheet(){
}
Next,youwillwanttospecifyits companionobjecttoholdclass-levelpropertiesthatcan
easilyberetrieved.Declareanew cssclass()-delegatedpropertycalled tackyButton,and
definefourcolorswewilluseforitsborders.
6.TypeSafeCSS
75
importjavafx.scene.paint.Color
importtornadofx.*
classMyStyle:Stylesheet(){
companionobject{
valtackyButtonbycssclass()
privatevaltopColor=Color.RED
privatevalrightColor=Color.DARKGREEN
privatevalleftColor=Color.ORANGE
privatevalbottomColor=Color.PURPLE
}
}
Notealsoyoucanusethe c()functiontobuildcolorsquicklyusingRGBvaluesorcolor
Strings.
privatevaltopColor=c("#FF0000")
privatevalrightColor=c("#006400")
privatevalleftColor=c("#FFA500")
privatevalbottomColor=c("#800080")
Finally,declarean init()blocktoapplystylingtotheclasses.Defineyourselectionand
provideablockthatmanipulatesitsvariousproperties.(Forcompoundselections,callthe
s()function,whichisanaliasforthe select()function).Set rotateto10degrees,
definethe borderColorusingthefourcolorsandthe box()function,makethefontfamily
"ComicSansMS",andincreasethe fontSizeto20pixels.Notethatthereareextension
propertiesfor Numbertypestoquicklyyieldthevalueinthatunit,suchas 10.degfor10
degreesand 20.pxfor20pixels.
6.TypeSafeCSS
76
importjavafx.scene.paint.Color
importtornadofx.*
classMyStyle:Stylesheet(){
companionobject{
valtackyButtonbycssclass()
privatevaltopColor=Color.RED
privatevalrightColor=Color.DARKGREEN
privatevalleftColor=Color.ORANGE
privatevalbottomColor=Color.PURPLE
}
init{
tackyButton{
rotate=10.deg
borderColor+=box(topColor,rightColor,bottomColor,leftColor)
fontFamily="ComicSansMS"
fontSize=20.px
}
}
}
Nowyoucanapplythe tackyButtonstyletobuttons,labels,andothercontrolsthatsupport
theseproperties.Whilethisstylingcanworkwithothercontrolslikelabels,wearegoingto
targetbuttonsinthisexample.
First,loadthe MyStylestylesheetintoyourapplicationbyincludingitascontructor
parameter.
classMyApp:App(MyView::class,MyStyle::class){
init{
reloadStylesheetsOnFocus()
}
}
The reloadStylesheetsOnFocus()functioncallwillinstructTornadoFXtoreloadthe
Stylesheetseverytimethe Stagegetsfocus.Youcanalsopassthe --live-
stylesheetsargumenttotheapplicationtoaccomplishthis.
Important:Forthereloadtowork,youmustberunningtheJVMindebugmodeandyou
mustinstructyourIDEtorecompilebeforeyouswitchbacktoyourapp.Withoutthesesteps,
nothingwillhappen.Thisalsoappliesto reloadViewsOnFocus()whichissimilar,butreloads
thewholeviewinsteadofjustthestylesheet.Thisway,youcanevolveyourUIveryrapidly
ina"codechange,compile,refresh"manner.
6.TypeSafeCSS
77
Youcanapplystylesdirectlytoacontrolbycallingits addClass()function.Providethe
MyStyle.tackyButtonstyletotwobuttons(Figure6.2).
classMyView:View(){
overridevalroot=vbox{
button("PressMe"){
addClass(MyStyle.tackyButton)
}
button("PressMeToo"){
addClass(MyStyle.tackyButton)
}
}
}
Figure6.2
IntellijIDEAcanperformaquickfixtoimportmembervariables,allowing
addClass(MyStyle.tackyButton)tobeshortenedto addClass(tackyButton)ifyouprefer.
Youcanuse removeClass()toremovethespecifiedstyleaswell.
TargetingStylestoaType
OneofthebenefitsofusingpureKotlinisyoucantightlymanipulateUIcontrolbehaviorand
conditionsusingKotlincode.Forexample,youcanapplythestyletoany Buttonby
iteratingthroughacontrol's children,filteringforonlychildrenthatareButtons,and
applyingthe addClass()tothem.
classMyView:View(){
overridevalroot=vbox{
button("PressMe")
button("PressMeToo")
children.asSequence()
.filter{itisButton}
.forEach{it.addClass(MyStyle.tackyButton)}
}
}
6.TypeSafeCSS
78
Infact,manipulatingclassesonseveralnodesatonceissocommonthatTornadoFX
providesashortcutforit:
children.filter{itisButton}.addClass(MyStyle.tackyButton)}
Youcanalsotargetall Buttoninstancesinyourapplicationbyselectingandmodifyingthe
buttoninthe Stylesheet.ThiswillapplythestyletoallButtons.
importjavafx.scene.paint.Color
importtornadofx.*
classMyStyle:Stylesheet(){
companionobject{
valtackyButtonbycssclass()
privatevaltopColor=Color.RED
privatevalrightColor=Color.DARKGREEN
privatevalleftColor=Color.ORANGE
privatevalbottomColor=Color.PURPLE
}
init{
button{
rotate=10.deg
borderColor+=box(topColor,rightColor,leftColor,bottomColor)
fontFamily="ComicSansMS"
fontSize=20.px
}
}
}
importjavafx.scene.layout.VBox
importtornadofx.*
classMyApp:App(MyView::class,MyStyle::class){
init{
reloadStylesheetsOnFocus()
}
}
classMyView:View(){
overridevalroot=vbox{
button("PressMe")
button("PressMeToo")
}
}
6.TypeSafeCSS
79
Figure6.3
Notealsoyoucanselectmultipleclassesandcontroltypestomix-and-matchstyles.For
example,youcansetthefontsizeoflabelsandbuttonsto20pixels,andcreatetacky
bordersandfontsonlyforbuttons(Figure6.4).
classMyStyle:Stylesheet(){
companionobject{
privatevaltopColor=Color.RED
privatevalrightColor=Color.DARKGREEN
privatevalleftColor=Color.ORANGE
privatevalbottomColor=Color.PURPLE
}
init{
s(button,label){
fontSize=20.px
}
button{
rotate=10.deg
borderColor+=box(topColor,rightColor,leftColor,bottomColor)
fontFamily="ComicSansMS"
}
}
}
classMyApp:App(MyView::class,MyStyle::class){
init{
reloadStylesheetsOnFocus()
}
}
classMyView:View(){
overridevalroot=vbox{
label("LoremIpsum")
button("PressMe")
button("PressMeToo")
}
}
6.TypeSafeCSS
80
Figure6.4
Multi-ValueCSSProperties
SomeCSSpropertiesacceptmultiplevalues,andTornadoFXStylesheetscanstreamline
thiswiththe multi()function.Thisallowsyoutospecifymultiplevaluesviaa varargs
parameterandletTornadoFXtakecareoftherest.Forinstance,youcannestmultiple
backgroundcolorsandinsetsintoacontrol(Figure6.5).
label("LoreIpsum"){
style{
fontSize=30.px
backgroundColor=multi(Color.RED,Color.BLUE,Color.YELLOW)
backgroundInsets=multi(box(4.px),box(8.px),box(12.px))
}
}
Figure6.5
The multi()functionshouldworkwherevermultiplevaluesareaccepted.Ifyouwantto
onlyassignasinglevaluetoapropertythatacceptsmultiplevalues,youwillneedtousethe
plusAssign()operatortoaddit(Figure6.6).
6.TypeSafeCSS
81
label("LoreIpsum"){
style{
fontSize=30.px
backgroundColor+=Color.RED
backgroundInsets+=box(4.px)
}
}
Figure6.6
NestingStyles
Insideaselectorblockyoucanapplyfurtherstylestargetingchildcontrols.
Forinstance,defineaCSSclasscalled critical.Makeitputanorangeborderaroundany
controlitisappliedto,andpaditby5pixels.
classMyStyle:Stylesheet(){
companionobject{
valcriticalbycssclass()
}
init{
critical{
borderColor+=box(Color.ORANGE)
padding=box(5.px)
}
}
}
Butsupposewhenweapplied criticaltoanycontrol,suchasan HBox,wewantittoadd
additionalstylingstobuttonsinsidethatcontrol.Nestinganotherselectionwilldothetrick.
6.TypeSafeCSS
82
classMyStyle:Stylesheet(){
companionobject{
valcriticalbycssclass()
}
init{
critical{
borderColor+=box(Color.ORANGE)
padding=box(5.px)
button{
backgroundColor+=Color.RED
textFill=Color.WHITE
}
}
}
}
Nowwhenyouapply criticaltosay,an HBox,allbuttonsinsidethat HBoxwillgetthat
definedstylefor button(Figure6.7)
classMyApp:App(MyView::class,MyStyle::class){
init{
reloadStylesheetsOnFocus()
}
}
classMyView:View(){
overridevalroot=hbox{
addClass(MyStyle.critical)
button("Warning!")
button("Danger!")
}
}
Figure6.7
Thereisonecriticalthingtonotconfusehere.TheorangeborderisonlyappliedtotheHBox
sincethe criticalclasswasappliedtoit.Thebuttonsdonotgetanorangeborder
becausetheyarechildrentothe HBox.Whiletheirstyleisdefinedby critical,theydo
notinheritthestylesoftheirparent,onlythosedefinedfor button.
6.TypeSafeCSS
83
Ifyouwantthebuttonstogetanorangebordertoo,youneedtoapplythe criticalclass
directlytothem.Youwillwanttousethe and()toapplyspecificstylestobuttonsthatare
alsodeclaredas critical.
classMyStyle:Stylesheet(){
companionobject{
valcriticalbycssclass()
}
init{
critical{
borderColor+=box(Color.ORANGE)
padding=box(5.px)
and(button){
backgroundColor+=Color.RED
textFill=Color.WHITE
}
}
}
}
classMyApp:App(MyView::class,MyStyle::class){
init{
reloadStylesheetsOnFocus()
}
}
classMyView:View(){
overridevalroot=hbox{
addClass(MyStyle.critical)
button("Warning!"){
addClass(MyStyle.critical)
}
button("Danger!"){
addClass(MyStyle.critical)
}
}
}
Figure6.8
6.TypeSafeCSS
84
Nowyouhaveorangebordersaroundthe HBoxaswellasthebuttons.Whennesting
styles,keepinmindthatwrappingtheselectionwith and()willcascadestylestochildren
controlsorclasses.
Mixins
Therearetimesyoumaywanttoreuseasetofstylingsandapplythemtoseveralcontrols
andselectors.Thispreventsyoufromhavingtoredundantlydefinethesamepropertiesand
values.Forinstance,ifyouwanttocreateasetofstylingcalled redAllTheThings,youcould
defineitasamixinasshownbelow.Thenyoucanreuseitfora redStyleclass,aswellas
a textInput,a label,anda passwordFieldwithadditionalstylemodifications(Figure
6.9).
Stylesheet
6.TypeSafeCSS
85
importjavafx.scene.paint.Color
importjavafx.scene.text.FontWeight
importtornadofx.*
classStyles:Stylesheet(){
companionobject{
valredStylebycssclass().
}
init{
valredAllTheThings=mixin{
backgroundInsets+=box(5.px)
borderColor+=box(Color.RED)
textFill=Color.RED
}
redStyle{
+redAllTheThings
}
s(textInput,label){
+redAllTheThings
fontWeight=FontWeight.BOLD
}
passwordField{
+redAllTheThings
backgroundColor+=Color.YELLOW
}
}
}
AppandView
6.TypeSafeCSS
86
classMyApp:App(MyView::class,Styles::class)
classMyView:View("MyView"){
overridevalroot=vbox{
label("Enteryourlogin")
form{
fieldset{
field("Username"){
textfield()
}
field("Password"){
passwordfield()
}
}
}
button("Go!"){
addClass(Styles.redStyle)
}
}
}
Figure6.9
Thestylesheetisappliedtotheapplicationbyaddingitasaconstructorparametertothe
Appclass.Thisisavarargparameter,soyoucansendinacommaseparatedlistofmultiple
stylesheets.Ifyouwanttoloadstylesheetsdynamicallybasedonsomecondition,youcan
call importStylesheet(Styles::classfromanywhere.AnyUIComponentopenedafterthe
callto importStylesheetwillgetthestylesheetapplied.Youcanalsoloadnormaltextbased
cssstylesheetswiththisfunction:
importStylesheet("/mystyles.css")
Loadingatextbasedcssstylesheet
IfyoufindyouarerepeatingyourselfsettingthesameCSSpropertiestothesamevalues,
youmightwanttoconsiderusingmixinsandreusingthemwherevertheyareneededina
Stylesheet.
6.TypeSafeCSS
87
ModifierSelections
TornadoFXalsosupportsmodifierselectionsbyleveraging and()functionswithina
selection.Themostcommoncasethisishandyisstylingfor"selected"andcursor"hover"
contextsforacontrol.
IfyouwantedtocreateaUIthatwillmakeany Buttonredwhenitishoveredover,andany
selected Cellindatacontrolssuchas ListViewred,youcandefinea Stylesheetlike
this(Figure6.10).
Stylesheet
importjavafx.scene.paint.Color
importtornadofx.Stylesheet
classStyles:Stylesheet(){
init{
button{
and(hover){
backgroundColor+=Color.RED
}
}
cell{
and(selected){
backgroundColor+=Color.RED
}
}
}
}
AppandView
importtornadofx.*
classMyApp:App(MyView::class,Styles::class)
classMyView:View("MyView"){
vallistItems=listOf("Alpha","Beta","Gamma").observable()
and
overridevalroot=vbox{
button("Hoveroverme")
listview(listItems)
}
}
6.TypeSafeCSS
88
Figure6.10-Acellisselectedandthe Buttonisbeinghoveredover.Botharenowred.
Wheneveryouneedmodifiers,usethe select()functiontomakethosecontextualstyle
modifications.
Control-SpecificStylesheets
Ifyoudecidetocreateyourowncontrols(oftenbyextendinganexistingcontrol,like
Button),JavaFXallowsyoutopairastylesheetwithit.Inthissituation,itisadvantageous
toloadthis Stylesheetonlywhenthiscontrolisloaded.Forinstance,ifyouhavea
DangerButtonclassthatextends Button,youmightconsidercreatinga Stylesheet
specificallyforthat DangerButton.ToallowJavaFXtoloadit,youneedtooverridethe
getUserAgentStyleSheet()functionasshownbelow.Thiswillconvertyourtype-safe
StylesheetintoplaintextCSSthatJavaFXnativelyunderstands.
classDangerButton:Button("Danger!"){
init{
addClass(DangerButtonStyles.dangerButton)
}
overridefungetUserAgentStylesheet()=DangerButtonStyles().base64URL.toExternalF
orm()
}
classDangerButtonStyles:Stylesheet(){
companionobject{
valdangerButtonbycssclass()
}
init{
dangerButton{
backgroundInsets+=box(0.px)
fontWeight=FontWeight.BOLD
fontSize=20.px
padding=box(10.px)
}
}
}
6.TypeSafeCSS
89
The DangerButtonStyles().base64URL.toExternalForm()expressioncreatesaninstanceofthe
DangerButtonStyles,andturnsitintoaURLcontainingtheentirestylesheetthatJavaFX
canconsume.
Conclusion
TornadoFXdoesagreatjobexecutingabrilliantconcepttomakeCSStype-safe,andit
furtherdemonstratesthepowerofKotlinDSL's.Configurationthroughstatictextfilesisslow
toexpresswith,buttype-safeCSSmakesitfluentandquickespeciallywithIDEauto-
completion.EvenifyouarepragmaticaboutUI'sandfeelstylingissuperfluous,therewillbe
timesyouneedtoleverageconditionalformattingandhighlightingsorules"popout"inaUI.
Atminimum,getcomfortableusingtheinline style{}blocksoyoucanquicklyaccess
stylingpropertiesthatcannotbeaccessedanyotherway(suchas TextWeight).
6.TypeSafeCSS
90
LayoutsandMenus
ComplexUI'srequiremanycontrols.Itislikelythesecontrolsneedtobegrouped,
positioned,andsizedwithsetpolicies.FortunatelyTornadoFXstreamlinesmanylayoutsthat
comewithJavaFX,aswellasfeaturesitsownproprietary Formlayout.
TornadoFXalsohastype-safebuilderstocreatemenusinahighlystructured,declarative
way.MenuscanbeespeciallycumbersometobuildusingconventionalJavaFXcode,and
Kotlinreallyshinesinthisdepartment.
BuildersforLayouts
Layoutsgroupcontrolsandsetpoliciesabouttheirsizingandpositioningbehavior.
Technically,layoutsthemselvesarecontrolssothereforeyoucannestlayoutsinside
layouts.ThisiscriticalforbuildingcomplexUI's,andTornadoFXmakesmaintenanceofUI
codeeasierbyvisiblyshowingthenestedrelationships.
VBox
A VBoxstackscontrolsverticallyintheordertheyaredeclaredinsideitsblock(Figure7.1).
vbox{
button("Button1").setOnAction{
println("Button1Pressed")
}
button("Button2").setOnAction{
println("Button2Pressed")
}
}
Figure7.1
Youcanalsocall vboxConstraints()withinachild'sblocktochangethemarginandvertical
growingbehaviorsofthe VBox.
7.LayoutsandMenus
91
vbox{
button("Button1"){
vboxConstraints{
marginBottom=20.0
vGrow=Priority.ALWAYS
}
}
button("Button2")
}
Youcanuseashorthandextensionpropertyfor vGrowwithoutcalling vboxConstraints().
vbox{
button("Button1"){
vGrow=Priority.ALWAYS
}
button("Button2")
}
HBox
HBoxbehavesalmostidenticallyto VBox,butitstacksallcontrolshorizontallyleft-to-right
intheorderdeclaredinitsblock.
hbox{
button("Button1").setOnAction{
println("Button1Pressed")
}
button("Button2").setOnAction{
println("Button2Pressed")
}
}
Figure7.2
Youcanalsocall hboxconstraints()withintheachild'sblocktochangethemarginand
horizontalgrowingbehaviorsofthe HBox.
7.LayoutsandMenus
92
hbox{
button("Button1"){
hboxConstraints{
marginRight=20.0
hGrow=Priority.ALWAYS
}
}
button("Button2")
}
Youcanuseashorthandextensionpropertyfor hGrowwithoutcalling hboxConstraints().
hbox{
button("Button1"){
hGrow=Priority.ALWAYS
}
button("Button2")
}
FlowPane
The FlowPanelaysoutcontrolsleft-to-rightandwrapstothenextlineontheboundary.For
example,sayyouadded100buttonstoa FlowPane(Figure7.3).Youwillnoticeitsimply
laysoutbuttonsfromleft-to-right,andwhenitrunsoutofroomitmovestothe"nextline".
flowpane{
for(iin1..100){
button(i.toString()){
setOnAction{println("Youpressedbutton$i")}
}
}
}
Figure7.3
7.LayoutsandMenus
93
Noticealsowhenyouresizethewindow,the FlowLayoutwillre-wrapthebuttonssotheyall
canfit(Figure7.4)
Figure7.4
The FlowLayoutisnotusedoftenbecauseitisoftensimplisticforhandlingalargenumber
ofcontrols,butitcomesinhandyforcertainsituationsandcanbeusedinsideotherlayouts.
BorderPane
The BorderPaneisahighlyusefullayoutthatdividescontrolsinto5regions: top, left,
bottom, right,and center.ManyUI'scaneasilybebuiltusingtwoormoreofthese
regionstoholdcontrols(Figure7.5).
7.LayoutsandMenus
94
borderpane{
top=label("TOP"){
useMaxWidth=true
style{
backgroundColor=Color.RED
}
}
bottom=label("BOTTOM"){
useMaxWidth=true
style{
backgroundColor=Color.BLUE
}
}
left=label("LEFT"){
useMaxWidth=true
style{
backgroundColor=Color.GREEN
}
}
right=label("RIGHT"){
useMaxWidth=true
style{
backgroundColor=Color.PURPLE
}
}
center=label("CENTER"){
useMaxWidth=true
style{
backgroundColor=Color.YELLOW
}
}
}
FIGURE7.5
Youwillnoticethatthe topand bottomregionstakeuptheentirehorizontalspace,while
left, center,and rightmustsharetheavailablehorizontalspace.But centeris
entitledtoanyextraavailablespace(verticallyandhorizontally),makingitidealtoholdlarge
controlslike TableView.Forinstance,youmayverticallystacksomebuttonsinthe left
regionandputa TableViewinthe centerregion(Figure7.6).
7.LayoutsandMenus
95
borderpane{
left=vbox{
button("REFRESH")
button("COMMIT")
}
center=tableview<Person>{
items=listOf(
Person("JoeThompson",33),
Person("SamSmith",29),
Person("NancyReams",41)
).observable()
column("NAME",Person::name)
column("AGE",Person::age)
}
}
Figure7.6
BorderPaneisalayoutyouwilllikelywanttouseoftenbecauseitsimplifiesmanycomplex
UI's.The topregioniscommonlyusedtoholda MenuBarandthe bottomregionoften
holdsastatusbarofsomekind.Youhavealreadyseen centerholdthefocalcontrolsuch
asa TableView,and leftand rightholdsidepanelswithanyperipheralcontrols(like
ButtonsorToolbars)notappropriateforthe MenuBar.WewilllearnaboutMenuslaterinthis
section.
FormBuilder
TornadoFXhasahelpful Formcontroltohandlealargenumberofuserinputs.Having
severalinputfieldstotakeuserinformationiscommonandJavaFXdoesnothaveabuilt-in
solutiontostreamlinethis.Toremedythis,TornadoFXhasabuildertodeclarea Formwith
anynumberoffields(Figure7.7).
7.LayoutsandMenus
96
form{
fieldset("PersonalInfo"){
field("FirstName"){
textfield()
}
field("LastName"){
textfield()
}
field("Birthday"){
datepicker()
}
}
fieldset("Contact"){
field("Phone"){
textfield()
}
field("Email"){
textfield()
}
}
button("Commit"){
action{println("Wrotetodatabase!")}
}
}
Figure7.7
7.LayoutsandMenus
97
7.LayoutsandMenus
98
Awesomeright?Youcanspecifyoneormorecontrolsforeachofthefields,andthe Form
willrenderthegroupingsandlabelsforyou.
Youcanchoosetolayoutthelabelabovetheinputsaswell:
fieldset("FieldSet",labelPosition=VERTICAL)
Each fieldrepresentsacontainerwiththelabelandanothercontainerfortheinputfields
youaddinsideit.Theinputcontainerisbydefaultan HBox,meaningthatmultipleinputs
withinasinglefieldwillbelaidoutnexttoeachother.Youcanspecifythe orientation
parametertoafieldtomakeitlayoutmultipleinputsbeloweachother.Anotherusecasefor
Verticalorientationistoallowaninputtogrowastheformexpandsvertically.Thisishandy
fordisplayingTextAreasinForms:
form{
fieldset("FeedbackForm",labelPosition=VERTICAL){
field("Comment",VERTICAL){
textarea{
prefRowCount=5
vgrow=Priority.ALWAYS
}
}
buttonbar{
button("Send")
}
}
}
Figure7.8
7.LayoutsandMenus
99
Theexampleabovealsousesthe buttonbarbuildertocreateaspecialfieldwithnolabel
whileretainingthelabelindentsothebuttonslineupundertheinputs.
Youbindeachinputtoamodel,andyoucanleavetherenderingofthecontrollayoutstothe
Form.Forthisreasonyouwilllikelywanttousethisoverthe GridPaneifpossible,which
wewillcovernext.
NestinglayoutsinsideaForm
Youcanwrapbothfieldsetsandfieldswithanylayoutcontainerofyourchoosingtocreate
complexformlayouts.
7.LayoutsandMenus
100
form{
hbox(20){
fieldset("LeftFieldSet"){
hbox(20){
vbox{
field("Fieldl1a"){textfield()}
field("Fieldl2a"){textfield()}
}
vbox{
field("Fieldl1b"){textfield()}
field("Fieldl2b"){textfield()}
}
}
}
fieldset("RightFieldSet"){
hbox(20){
vbox{
field("Fieldr1a"){textfield()}
field("Fieldr2a"){textfield()}
}
vbox{
field("Fieldr1b"){textfield()}
field("Fieldr2b"){textfield()}
}
}
}
}
}
TheHBoxesareconfiguredwithaspacingof20pixels,usingtheparameterforthe hbox
builder.Itcanalsobespecifiedas hbox(spacing=20)forclarity.
Figure7.9
GridPane
Ifyouwanttomicromanagethelayoutofyourcontrols,the GridPanewillgiveyouplentyof
that.Ofcourseitrequiresmoreconfigurationandcodeboilerplate.Beforeproceedingtouse
a GridPane,youmightwanttoconsiderusing Formorotherlayoutsthatabstractlayout
configurationforyou.
7.LayoutsandMenus
101
Onewaytouse GridPaneistodeclarethecontentsofeach row.Foranygiven Nodeyou
cancallits gridpaneConstraintstoconfigurevarious GridPanebehaviorsforthat Node,
suchas marginand columnSpan(Figure7.10)
gridpane{
row{
button("North"){
useMaxWidth=true
gridpaneConstraints{
marginBottom=10.0
columnSpan=2
}
}
}
row{
button("West")
button("East")
}
row{
button("South"){
useMaxWidth=true
gridpaneConstraints{
marginTop=10.0
columnSpan=2
}
}
}
}
Figure7.11
Noticehowthereisamarginof 10.0betweeneachrow,whichwasdeclaredforthe
marginBottomand marginTopofthe"North"and"South"buttonsrespectivelyinsidetheir
gridpaneConstraints.
Alternatively,youcanexplicitlyspecifythecolumn/rowindexpositionsforeach Noderather
thandeclaringeach rowofcontrols.Thiswillaccomplishtheexactlayoutwebuilt
previously,butwithcolumn/rowindexspecificationsinstead.Itisabitmoreverbose,butit
7.LayoutsandMenus
102
givesyoumoreexplicitcontroloverthepositionsofcontrols.
gridpane{
button("North"){
useMaxWidth=true
gridpaneConstraints{
columnRowIndex(0,0)
marginBottom=10.0
columnSpan=2
}
}
button("West").gridpaneConstraints{
columnRowIndex(0,1)
}
button("East").gridpaneConstraints{
columnRowIndex(1,1)
}
button("South"){
useMaxWidth=true
gridpaneConstraints{
columnRowIndex(0,2)
marginTop=10.0
columnSpan=2
}
}
}
Theseareallthe gridpaneConstraintsattributesyoucanmodifyonagiven Node.Some
areexpressedassimplepropertiesthatcanbeassignedwhileothersareassignable
throughfunctions.
7.LayoutsandMenus
103
Attribute Description
columnIndex:Int Thecolumnindexforthegivencontrol
rowIndex:Int Therowindexforthegivencontrol
columnRowIndex(columnIndex:Int,
rowIndex:Int) Specifestherowandcolumnindex
columnSpan:Int Thenumberofcolumnsthecontroloccupies
rowSpan:Int Thenumberofrowsthecontroloccupies
hGrow:Priority Thehorizonalgrowpriority
vGrow:Priority Theverticalgrowpriority
vhGrow:Priority Specifiesthesamepriorityfor vGrowand
hGrow
fillHeight:Boolean Setswhetherthe Nodefillstheheightofits
area
fillWidth:Boolean Setswhetherthe Nodefillesthewidthofits
area
fillHeightWidth:Boolean Setswhetherthe Nodefillsitsareaforboth
heightandwidth
hAlignment:HPos Thehorizonalalignmentpolicy
vAlignment:VPos Theverticalalignmentpolicy
margin:Int Themarginforallfoursidesofthe Node
marginBottom:Int Themarginforthebottomsideofthe Node
marginTop:Int Themarginforthetopsideofthe Node
marginLeft:Int Theleftmarginfortheleftsideofthe Node
marginRight:Int Therightmarginfortherightsideofthe Node
marginLeftRight:Int Therightandleftmarginsforthe Node
marginTopBottom:Int Thetopandbottommarinsfora Node
Additionally,ifyouneedtoconfigure ColumnConstraints,youcancall
gridpaneColumnConstraintsonanychild Node,or constraintsForColumn(columnIndex)on
the GridPaneitself.
7.LayoutsandMenus
104
gridpane{
row{
button("Left"){
gridpaneColumnConstraints{
percentWidth=25.0
}
}
button("Middle")
button("Right")
}
constraintsForColumn(1).percentWidth=50.0
}
StackPane
A StackPaneisalayoutyouwilluselessoften.Foreachcontrolyouadd,itwillliterallystack
themontopofeachothernotlikea VBox,butliterallyoverlaythem.
Forinstance,youcancreatea"BOTTOM" Buttonandputa"TOP" Buttonontopofit.
Theorderyoudeclarecontrolswilladdthemfrombottom-to-topinthatsameorder(Figure
7.10).
classMyView:View(){
overridevalroot=stackpane{
button("BOTTOM"){
useMaxHeight=true
useMaxWidth=true
style{
backgroundColor+=Color.AQUAMARINE
fontSize=40.0.px
}
}
button("TOP"){
style{
backgroundColor+=Color.WHITE
}
}
}
}
Figure7.11
7.LayoutsandMenus
105
TabPane
A TabPanecreatesaUIwithdifferentscreensseparatedby"tabs".Thisallowsswitching
betweendifferentscreensquicklyandeasilybyclickingonthecorrespondingtab(Figure
7.11).Youcandeclarea tabpane()andthendeclareasmany tab()instancesasyou
need.Foreach tab()functionpassinthenameofthe Tabandtheparent Nodecontrol
topopulateit.
tabpane{
tab("Screen1",VBox()){
button("Button1")
button("Button2")
}
tab("Screen2",HBox()){
button("Button3")
button("Button4")
}
}
Figure7.12
TabePaneisaneffectivetooltoseparatescreensandorganizeahighnumberofcontrols.
Thesyntaxissomewhatsuccinctenoughtodeclarecomplexcontrolslike TableViewright
insidethe tab()block(Figure7.13).
7.LayoutsandMenus
106
tabpane{
tab("Screen1",VBox()){
button("Button1")
button("Button2")
}
tab("Screen2",HBox()){
tableview<Person>{
items=listOf(
Person(1,"SamanthaStuart",LocalDate.of(1981,12,4)),
Person(2,"TomMarks",LocalDate.of(2001,1,23)),
Person(3,"StuartGills",LocalDate.of(1989,5,23)),
Person(3,"NicoleWilliams",LocalDate.of(1998,8,11))
).observable()
column("ID",Person::id)
column("Name",Person::name)
column("Birthday",Person::birthday)
column("Age",Person::age)
}
}
}
Figure7.13
7.LayoutsandMenus
107
Likemanybuilders,the TabPanehasseveralpropertiesthatcanadjustthebehaviorofits
tabs.Forinstance,youcancall tabClosingPolicytogetridofthe"X"buttonsonthetabsso
theycannotbeclosed.
classMyView:View(){
overridevalroot=tabpane{
tabClosingPolicy=TabPane.TabClosingPolicy.UNAVAILABLE
tab("Screen1",VBox()){
button("Button1")
button("Button2")
}
tab("Screen2",HBox()){
button("Button3")
button("Button4")
}
}
}
BuildersforMenus
Creatingmenuscanbecumbersometobuildinastrictlyobject-orientedway.Butusing
type-safebuilders,Kotlin'sfunctionalconstructsmakeitintuitivetodeclarenestedmenu
hierarchies.
MenuBar,Menu,andMenuItem
Itisnotuncommontousenavigablemenustokeepalargenumberofcommandsonauser
interfaceorganized.Forinstance,the topregionofa BorderPaneistypicallywherea
MenuBargoes.Thereyoucanaddmenusandsubmenuseasily(Figure7.5).
menubar{
menu("File"){
menu("Connect"){
item("Facebook")
item("Twitter")
}
item("Save")
item("Quit")
}
menu("Edit"){
item("Copy")
item("Paste")
}
}
7.LayoutsandMenus
108
Figure7.14
Youcanalsooptionallyprovidekeyboardshortcuts,graphics,aswellasan actionfunction
parameterforeach item()tospecifytheactionwhenitisselected(Figure7.14).
menubar{
menu("File"){
menu("Connect"){
item("Facebook",graphic=fbIcon).action{println("ConnectingFacebook!"
)}
item("Twitter",graphic=twIcon).action{println("ConnectingTwitter!")
}
}
item("Save","Shortcut+S").action{
println("Saving!")
}
menu("Quit","Shortcut+Q").action{
println("Quitting!")
}
}
menu("Edit"){
item("Copy","Shortcut+C").action{
println("Copying!")
}
item("Paste","Shortcut+V").action{
println("Pasting!")
}
}
}
Figure7.14
7.LayoutsandMenus
109
Separators
Youcandeclarea separator()betweentwoitemsina Menutocreateadividerline.This
ishelpfultogroupcommandsina Menuanddistinctlyseparatethem(Figure7.15).
menu("File"){
menu("Connect"){
item("Facebook")
item("Twitter")
}
separator()
item("Save","Shortcut+S"){
println("Saving!")
}
item("Quit","Shortcut+Q"){
println("Quitting!")
}
}
Figure7.15
7.LayoutsandMenus
110
ContextMenu
MostcontrolsinJavaFXhavea contextMenupropertywhereyoucanassigna ContextMenu
instance.Thisisa Menuthatpopsupwhenthecontrolisright-clicked.
A ContextMenuhasfunctionstoadd Menuand MenuIteminstancestoitjustlikea
MenuBar.Itcanbehelpfultoadda ContextMenutoa TableView<Person>,forexample,and
providecommandstobedoneonatablerecord(Figure7.16).Thereisabuildercalled
contextmenuthatwillbuilda ContextMenuandassignittothe contextMenupropertyofthe
control.
tableview(persons){
column("ID",Person::id)
column("Name",Person::name)
column("Birthday",Person::birthday)
column("Age",Person::age)
contextmenu{
item("SendEmail").action{
selectedItem?.apply{println("SendingEmailto$name")}
}
item("ChangeStatus").action{
selectedItem?.apply{println("ChangingStatusfor$name")}
}
}
}
Figure7.16
7.LayoutsandMenus
111
Notetherearealso RadioMenuItemand CheckMenuItemvariantsof MenuItemavailable.
The menuitembuilderstaketheactiontoperformwhenthemenuisselectedastheop
blockparameter.Unfortunately,thisbreakswiththeotherbuilders,wheretheopblock
operatesontheelementthatthebuildercreated.Therefore,the itembuilderwas
introducedasanalternative,whereyouoperateontheitemitself,sothatyoumustcall
setOnActiontoassigntheaction.The menuitembuilderisnotdeprecated,asitsolvesthe
commoncaseinamoreconcisewaythanthe itembuilder.
ListMenu
TornadoFXcomeswithalistmenuthatbehavesandlooksmorelikeatypical ul/libased
HTML5menu.
7.LayoutsandMenus
112
Thefollowingcodeexampleshowshowtousethe ListMenuwiththebuilderpattern:
listmenu(theme="blue"){
item(text="Contacts",graphic=Styles.contactsIcon()){
//Marksthisitemasactive.
activeItem=this
whenSelected{/*Dosomeaction*/}
}
item(text="Projects",graphic=Styles.projectsIcon())
item(text="Settings",graphic=Styles.settingsIcon())
}
ThefollowingAttributescanbeusedtoconfigurethe ListMenu:
7.LayoutsandMenus
113
Attribute Builder-
Attribute Type Default Description
orientation yes Orientation VERTICAL
Configuresthe
orientationofthe
ListMenu.Possible
orientations:
VERTICAL
HORIZONTAL
iconPosition yes Side LEFT
Configurestheicon
positionofthe ListMenu.
Possiblepositions:
TOP
BOTTOM
LEFT
RIGHT
theme yes String null
Currentlysupported
themes blue, null.If
nullissetthedefault
graythemeisused.
tag yes Any? null
TheTagcanbeany
objector null,itcan
beusefultoidentifythe
ListMenu
activeItem no ListMenuItem? null
Represent'sthecurrent
active ListMenuItemof
the ListMenu.Toselect
a ListMenuoncreation,
justassignthespecific
ListItemtothis
property(havealookat
thecontacts
ListMenuIteminthe
codeexampleabove.)
CssProperties
7.LayoutsandMenus
114
Css-Class Css-
Property Default Description
.list-menu -fx-graphic-
fixed-size 2em Thegraphic
size.
.list-menu
.list-item -fx-cursor hand Thecursor
symbol.
.list-menu
.list-item -fx-padding 10
Thepadding
foreach
item
.list-menu
.list-item
-fx-
background-
color
-fx-shadow-highlight-color,-fx-outer-
border,-fx-inner-border,-fx-body-
color
Thecolorof
the item
.list-menu
.list-item
-fx-
background-
insets
00-0.50,0,0.5,1.5 Theinsetsof
each item.
.list-menu
.list-item
.label -fx-text-fill -fx-text-base-color
Thetext
colorofeach
item.
PseudoClasses
Pseudo-
Class
Css-
Property Default Description
.list-menu
.list-
item:active
-fx-
background-
color
-fx-focus-color,-fx-inner-border,-
fx-body-color,-fx-faint-focus-color,
-fx-body-color
Thecolorwillbe
setifthe item
isactive.
.list-menu
.list-
item:active
-fx-
background-
insets
-0.2,1,2,-1.4,2.6
Insetswillbeset
ifthe itemis
active.
.list-menu
.list-
item:hover -fx-color -fx-hover-base Thehovercolor.
HavealookatthedefaultStylesheetfortheListMenu
Item
The itembuilderallowstocreate itemsforthe ListMenuinaveryconvenientway.The
followingsyntaxissupported:
7.LayoutsandMenus
115
item("SomeText",graphic=SomeNode,tag=SomeObject){
//Marksthisitemasactive.
activeItem=this
//Dosomeactionwhenselected
whenSelected{/*Action*/}
}
Attribute Builder-
Attribute Type Default Description
text yes String? null Thetextwhichshouldbesetforthe
given item.
tag yes Any? null
TheTagcanbeany objector null
andcanbeusefultoidentifythe
ListItem
graphic yes Node? null
The graphiccanbeany Nodeand
willbedisplayedbesidethegiven
text.
Function Description
whenSelected Aconvincefunction,whichwillbecalledanytimethegiven
ListMenuItemisselected.
Fillingtheparentcontainer
The useMaxWidthpropertycanbeusedtofilltheparentcontainerhorizontally.The
useMaxHeightpropertywillfilltheparentcontainervertically.Thesepropertiesactually
appliestoallNodes,butisespeciallyusefulforthe ListMenu.
SqueezeBox
JavaFXhasanAccordioncontrolthatletsyougroupasetof TilePanestogethertoforman
accordionofcontrols.TheJavaFXAccordiononlyletsyouopenasingleaccordionfoldata
time,andithassomeothershortcomings.Tosolvethis,TornadoFXcomeswiththe
SqueezeBoxcomponentthatbehavesandlooksverysimilartotheAccordion,while
providingsomeenhancements.
7.LayoutsandMenus
116
squeezebox{
fold("CustomerEditor",expanded=true){
form{
fieldset("CustomerDetails"){
field("Name"){textfield()}
field("Password"){textfield()}
}
}
}
fold("Someothereditor",expanded=true){
stackpane{
label("Nothinghere")
}
}
}
Figure7.17
ASqueezeboxshowingtwofolds,bothexpandedbydefault
YoucantelltheSqueezeBoxtoonlyallowasinglefoldtobeexpandedatanygiventimeby
passing multiselect=falsetothebuilderconstructor.
Youcanoptionallyallowfoldstobeclosablebyclickingacrossintherightcornerofthetitle
paneforthefold.Youenabletheclosebuttonsonaperfoldbasisbypassing closeable=
truetothe foldbuilder.
7.LayoutsandMenus
117
squeezebox{
fold("CustomerEditor",expanded=true,closeable=true){
form{
fieldset("CustomerDetails"){
field("Name"){textfield()}
field("Password"){textfield()}
}
}
}
fold("Someothereditor",closeable=true){
stackpane{
label("Nothinghere")
}
}
}
Figure7.18
ThisSqueezeBoxhascloseablefolds
The closeablepropertycanofcoursebecombinedwith expanded.
AnotherimportantdifferencebetweentheSqueezeBoxandtheAccordionisthewayit
distributesoverflowingspace.TheAccordionwillextendverticallytofillitsparentcontainer
andpushanyfoldsbelowthecurrentlyopenedonesallthewaytothebottom.Thiscreates
anunnaturallookingviewiftheparentcontainerisverylarge.Thesqueezeboxprobably
doeswhatyouwantbydefaultinthisregard,butyoucanadd fillHeight=truetogeta
similarlookastheAccordion.I
7.LayoutsandMenus
118
YoucanstyletheSqueezeBoxlikeyoustyleaTitlePane.Theclosebuttonhasacssclass
called close-buttonandthecontainerhasacssclasscalled squeeze-box.
Drawer
TheDrawerisanavigationcomponentmuchlikeaTabPane,butitorganizeseachdrawer
iteminaverticallyorhorizontallyplacedbuttonbaroneithersideoftheparentcontainer.It
resemblesthetooldrawersfoundinmanypopularbusinessapplicationsandIDEs.Whenan
itemisselected,thecontentfortheitemisdisplayednexttoorabove/belowthebuttonsina
contentareaspanningtheheightorwidthofthecontrolandthepreferredwidthorheightof
thecontent,dependingonwhetheritisdockedinaverticalorhorizontalsideoftheparent.
In multiselectmodeitwillevenletyouopenmultipledraweritemssimutaneouslyand
havethemsharethespacebetweenthem.Theywillalwaysopenintheorderofthe
correspondingbuttons.
7.LayoutsandMenus
119
classDrawerView:View("TornadoFXInfoBrowser"){
overridevalroot=drawer{
item("Screencasts",expanded=true){
webview{
prefWidth=470.0
engine.userAgent=iPhoneUserAgent
engine.load(TornadoFXScreencastsURI)
}
}
item("Links"){
listview(links){
cellFormat{link->
graphic=hyperlink(link.name){
setOnAction{
hostServices.showDocument(link.uri)
}
}
}
}
}
item("People"){
tableview(people){
column("Name",Person::name)
column("Nick",Person::nick)
}
}
}
classLink(valname:String,valuri:String)
classPerson(valname:String,valnick:String)
//Sampledatavariablesleftout(iPhoneUserAgent,TornadoFXScreencastsURI,people
andlinks)
}
Figure7.19
7.LayoutsandMenus
120
Thedrawercanbeconfiguredtoshowthebuttonsontherightside,andyoucanchooseto
supportopeningmultipledraweritemssimultaneously.Whenrunninginmultiselectmode,a
headerwillappearabovethecontent,whichwillhelptodistinguishtheitemsinthecontent
area.Youcancontroltheheaderappearancewiththeboolean showHeaderparameter.Itwill
defaulttruewhenmultiselectisenabledandfalseotherwise.
7.LayoutsandMenus
121
drawer(side=Side.RIGHT,multiselect=true){
//Everythingelseisidentical
}
Figure7.20
Drawerwithbuttonsontherightside,multiselectmodeandtitlepanes
WhentheDrawerisaddednexttosomething,youcanchoosewhetherthecontentareaof
theDrawershoulddisplacethenodesnexttoit(default)orfloatoverit.The
floatingContentpropertyisbydefaultfalse,causingtheDrawertodisplacethecontent
7.LayoutsandMenus
122
nexttoit.
Youcancontrolthesizeofthecontentareafurtherusingthe maxContentSizeand
fixedContentSizepropertiesof Drawer.Dependingonthe dockingSide,thoseproperties
willconstraineitherthewidthortheheightofthecontentarea.
The WorkspacefeaturesbuiltinsupportfortheDrawercontrol.The leftDrawer,
rightDrawerand bottomDrawerpropertiesofanyWorkspacewillletyoudockdraweritems
intothem.ReadmoreaboutthisintheWorkspacechapter.
Convertingobservablelistitemsandbinding
tolayouts
TODO
Summary
BynowyoushouldhavethetoolstoquicklycreatecomplexUI'swithlayouts,tabbedpanes,
aswellasothercontrolstomanagecontrols.Usingtheseinconjunctionwiththedata
controls,youshouldbeabletoturnaroundUI'sinafractionofthetime.
Whenitcomestobuilders,youhavereachedthetopofthepeakandhaveeverythingyou
needtobeproductive.Allthatislefttocoverarechartsandshapes,whichwewillcoverin
thenexttwochapters.
7.LayoutsandMenus
123
Charts
JavaFXcomeswithahandysetofchartstoquicklydisplaydatavisualizations.Whilethere
aremorecomprehensivechartinglibrarieslikeJFreeChartandOrsonChartswhichwork
finewithTornadoFX,thebuilt-inJavaFXchartssatisfyamajorityofvisualizationneeds.
Theyalsohaveelegantanimationswhendataispopulatedorchanged.
TornadoFXcomeswithafewbuilderstostreamlinethedeclarationofchartsusingfunctional
constructs.
PieChart
The PieChartisacommonvisualaidtoillustrateproportionsofawhole.Itisstructurally
simplerthanXYchartswhichwewilllearnaboutlater.Insidea piechart()builderyoucan
callthe data()functiontopassmultiplecategory-valuepairs(Figure8.1).
piechart("Desktop/LaptopOSMarketShare"){
data("Windows",77.62)
data("OSX",9.52)
data("Other",3.06)
data("Linux",1.55)
data("ChromeOS",0.55)
}
Figure8.1
8.Charts
124
Noteyoucanalsoprovideanexplicit ObservableList<PieChart.Data>preparedinadvance.
valitems=listOf(
PieChart.Data("Windows",77.62),
PieChart.Data("OSX",9.52),
PieChart.Data("Other",3.06),
PieChart.Data("Linux",1.55),
PieChart.Data("ChromeOS",0.55)
).observable()
piechart("Desktop/LaptopOSMarketShare",items)
Theblockfollowing piechartcanbeusedtomodifyanyoftheattributesofthe PieChart
justlikeanyothercontrolbuilderwecovered.Youcanalsoleverage for()loops,
Sequences,andotheriterativetoolswithinablocktoaddanynumberofdataitems.
8.Charts
125
valitems=listOf(
PieChart.Data("Windows",77.62),
PieChart.Data("OSX",9.52),
PieChart.Data("Other",3.06),
PieChart.Data("Linux",1.55),
PieChart.Data("ChromeOS",0.55)
).observable()
piechart("Desktop/LaptopOSMarketShare"){
for(iteminitems){
data.add(item)
}
}
Map-BasedDataSources
Sometimesyoumaywanttobuildachartusinga Mapasadatasource.UsingtheKotlin
tooperator,youcanconstructa MapinaKotlin-esquewayandthenpassittothe data
function.
valitems=mapOf(
"Windows"to77.62,
"OSX"to9.52,
"Other"to3.06,
"Linux"to1.55,
"ChromeOS"to0.55
)
piechart("Desktop/LaptopOSMarketShare"){
data(items)
}
XYBasedCharts
MostchartsoftendealwithoneormoreseriesofdatapointsonanXYaxis.Themost
commonarebarandlinecharts.
BarCharts
Youcanrepresentoneormoreseriesofdatapointsthrougha BarChart.Thischartmakes
iteasytocomparedifferentdatapointsrelativetotheirdistancefromtheXorYaxis(Figure
8.2).
8.Charts
126
barchart("UnitSalesQ22016",CategoryAxis(),NumberAxis()){
series("ProductX"){
data("MAR",10245)
data("APR",23963)
data("MAY",15038)
}
series("ProductY"){
data("MAR",28443)
data("APR",22845)
data("MAY",19045)
}
}
Figure8.2
Above,the series()and data()functionsallowquickconstructionofdatastructures
backingthecharts.Onconstruction,youwillneedtoconstructtheproper Axistypefor
eachXandYaxis.Inthisexample,themonthsarenotnecessarilynumericbutrather
Strings.Thereforetheyarebestrepresentedbya CategoryAxis.Theunits,alreadybeing
numeric,arefittousea NumberAxis.
8.Charts
127
Inthe series()and data()blocks,youcancustomizefurtherpropertieslikecolors.
Youcanevencall style()toquicklyapplytype-safeCSStothechart.
LineChartandAreaChart
A LineChartconnectsdatapointsonanXYaxiswithlines,quicklyvisualizingupwardand
downwardtrendsbetweenthem(Figure8.3)
linechart("UnitSalesQ22016",CategoryAxis(),NumberAxis()){
series("ProductX"){
data("MAR",10245)
data("APR",23963)
data("MAY",15038)
}
series("ProductY"){
data("MAR",28443)
data("APR",22845)
data("MAY",19045)
}
}
Figure8.3
8.Charts
128
Thebackingdatastructureisnotmuchdifferentthana BarChart,andyouusethe
series()and data()functionsinthesamemanner.
Youcanalsouseavariantof LineChartcalled AreaChart,whichwillshadetheareaunder
thelinesadistinctcolor,aswellasanyoverlaps(Figure8.4).
Figure8.4
8.Charts
129
Multiseries
Youcanstreamlinethedeclarationofmorethanoneseriesusingthe multiseries()
function,andcallthe data()functionswith varargsvalues.Wecanconsolidateour
previousexampleusingthisconstruct:
linechart("UnitSalesQ22016",CategoryAxis(),NumberAxis()){
multiseries("ProductX","ProductY"){
data("MAR",10245,28443)
data("APR",23963,22845)
data("MAY",15038,19045)
}
}
Thisisjustanotherconveniencetoreduceboilerplateandquicklydeclareyourdata
structureforachart.
8.Charts
130
ScatterChart
A ScatterChartisthesimplestrepresentationofanXYdataseries.Itplotsthepoints
withoutbarsorlines.Itisoftenusedtoplotalargevolumeofdatapointsinordertofind
clusters.Hereisabriefexampleofa ScatterChartplottingmachinecapacitiesbyweekfor
twodifferentproductlines(Figure8.5).
scatterchart("MachineCapacitybyProduct/Week",NumberAxis(),NumberAxis()){
series("ProductX"){
data(1,24)
data(2,22)
data(3,23)
data(4,19)
data(5,18)
}
series("ProductY"){
data(1,12)
data(2,15)
data(3,9)
data(4,11)
data(5,7)
}
}
Figure8.5
8.Charts
131
BubbleChart
BubbleChartisanotherXYchartsimilartothe ScatterPlot,butthereisathirdvariableto
controltheradiusofeachpoint.Youcanleveragethistoshow,forinstance,outputbyweek
withthebubbleradiireflectingnumberofmachinesused(Figure8.6).
8.Charts
132
bubblechart("MachineCapacitybyOutput/Week",NumberAxis(),NumberAxis()){
series("ProductX"){
data(1,24,1)
data(2,46,2)
data(3,23,1)
data(4,27,2)
data(5,18,1)
}
series("ProductY"){
data(1,12,1)
data(2,31,2)
data(3,9,1)
data(4,11,1)
data(5,15,2)
}
}
Figure8.6
8.Charts
133
Summary
Chartsareaaneffectivewaytovisualizedata,andthebuildersinTornadoFXhelpcreate
themquickly.YoucanreadmoreaboutJavaFXchartsinOracle'sdocumentation.Ifyou
needmoreadvancedchartingfunctionality,therearelibrarieslikeJFreeChartandOrson
ChartsyoucanleverageandinteropwithTornadoFX,butthisisbeyondthescopeofthis
book.
8.Charts
134
ShapesandAnimation
JavaFXcomeswithnodesthatrepresentalmostanygeometricshapeaswellasa Path
nodethatprovidesfacilitiesrequiredforassemblyandmanagementofageometricpath(to
createcustomshapes).JavaFXalsohasanimationsupporttograduallychangea Node
property,creatingavisualtransitionbetweentwostates.TornadoFXseekstostreamlineall
thesefeaturesthroughbuilderconstructs.
ShapeBasics
Everyparametertotheshapebuildersareoptional,andinmostcasesdefaulttoavalueof
0.0.Thismeansthatyouonlyneedtoprovidetheparametersyoucareabout.The
buildershavepositionalparametersformostofthepropertiesofeachshape,andtherest
canbesetinthefunctionalblockthatfollows.Thereforetheseareallvalidwaystocreatea
rectangle:
rectangle{
width=100.0
height=100.0
}
rectangle(width=100.0,height=100.0)
rectangle(0.0,0.0,100.0,100.0)
Theformyouchooseisamatterofpreference,butobviouslyconsiderthelegibilityofthe
codeyouwrite.Theexamplesinthischapterspecifymostofthepropertiesinsidethecode
blockforclarity,exceptwhenthereisnocodeblocksupportortheparametersare
reasonablyself-explanatory.
PositioningwithintheParent
Mostoftheshapebuildersgiveyoutheoptiontospecifythelocationoftheshapewithinthe
parent.Whetherornotthiswillhaveanyeffectdependsontheparentnode.An HBoxwill
notcareaboutthe xand ycoordinatesyouspecifyunlessyoucall setManaged(false)
ontheshape.However,a Groupcontrolwill.Thescreenshotsinthefollowingexampleswill
becreatedbywrappinga StackPanewithpaddingarounda Group,andfinallytheshape
wascreatedinsidethat Groupasshownbelow.
9.ShapesandAnimation
135
classMyView:View(){
overridevalroot=stackpane{
group{
//shapeswillgohere
}
}
}
Rectangle
Rectangledefinesarectanglewithanoptionalsizeandlocationintheparent.Rounded
cornerscanbespecifiedwiththe arcWidthand arcHeightproperties(Figure9.1).
rectangle{
fill=Color.BLUE
width=300.0
height=150.0
arcWidth=20.0
arcHeight=20.0
}
Figure9.1
9.ShapesandAnimation
136
Arc
Arcrepresentsanarcobjectdefinedbyacenter,startangle,angularextent(lengthofthe
arcindegrees),andanarctype( OPEN, CHORD,or ROUND)(Figure9.2)
arc{
centerX=200.0
centerY=200.0
radiusX=50.0
radiusY=50.0
startAngle=45.0
length=250.0
type=ArcType.ROUND
}
Figure9.2
Circle
Circlerepresentsacirclewiththespecified radiusand center.
circle{
centerX=100.0
centerY=100.0
radius=50.0
}
9.ShapesandAnimation
137
CubicCurve
CubicCurverepresentsacubicBézierparametriccurvesegmentin(x,y)coordinatespace.
Drawingacurvethatintersectsboththespecifiedcoordinates( startX, startY)and
(endX, enfY),usingthespecifiedpoints( controlX1, controlY1)and( controlX2,
controlY2)asBéziercontrolpoints.
cubiccurve{
startX=0.0
startY=50.0
controlX1=25.0
controlY1=0.0
controlX2=75.0
controlY2=100.0
endX=150.0
endY=50.0
fill=Color.GREEN
}
9.ShapesandAnimation
138
Ellipse
Ellipserepresentsanellipsewithparametersspecifyingsizeandlocation.
ellipse{
centerX=50.0
centerY=50.0
radiusX=100.0
radiusY=50.0
fill=Color.CORAL
}
Line
Lineisfairlystraightforward.Supplystartandendcoordinatestodrawalinebetweenthe
twopoints.
line{
startX=50.0
startY=50.0
endX=150.0
endY=100.0
}
9.ShapesandAnimation
139
Polyline
A Polylineisdefinedbyanarrayofsegmentpoints. Polylineissimilarto Polygon,
exceptitisnotautomaticallyclosed.
polyline(0.0,0.0,80.0,40.0,40.0,80.0)
QuadCurve
The QuadcurverepresentsaquadraticBézierparametriccurvesegmentin(x,y)coordinate
space.Drawingacurvethatintersectsboththespecifiedcoordinates( startX, startY)
and( endX, endY),usingthespecifiedpoint( controlX, controlY)asBéziercontrolpoint.
9.ShapesandAnimation
140
quadcurve{
startX=0.0
startY=150.0
endX=150.0
endY=150.0
controlX=75.0
controlY=0.0
fill=Color.BURLYWOOD
}
SVGPath
SVGPathrepresentsashapethatisconstructedbyparsingSVGpathdatafromaString.
svgpath("M70,50L90,50L120,90L150,50L170,50L210,90L180,120L170,110L170,200L70,
200L70,110L60,120L30,90L70,50"){
stroke=Color.DARKGREY
strokeWidth=2.0
effect=DropShadow()
}
9.ShapesandAnimation
141
Path
Pathrepresentsashapeandprovidesfacilitiesrequiredforbasicconstructionand
managementofageometricpath.Inotherwords,ithelpsyoucreateacustomshape.The
followinghelperfunctionscanbeusedtoconstuctthepath:
moveTo(x,y)
hlineTo(x)
vlineTo(y)
quadqurveTo(controlX,controlY,x,y)
lineTo(x,y)
arcTo(radiusX,radiusY,xAxisRotation,x,y,largeArcFlag,sweepFlag)
closepath()
9.ShapesandAnimation
142
path{
moveTo(0.0,0.0)
hlineTo(70.0)
quadqurveTo{
x=120.0
y=60.0
controlX=100.0
controlY=0.0
}
lineTo(175.0,55.0)
arcTo{
x=50.0
y=50.0
radiusX=50.0
radiusY=50.0
}
}
Animation
JavaFXhastoolstoanimateany Nodebygraduallychangingoneormoreofitsproperties.
TherearethreecomponentsyouworkwithtocreateanimationsinJavaFX.
Timeline-Asequenceof KeyFrameitemsexecutedinaspecifiedorder
KeyFrame-A"snapshot"specifyingvaluechangesononeormorewritableproperties
(viaa KeyValue)ononeormoreNodes
KeyValue-Apairingofa Nodepropertytoavaluethatwillbe"transitioned"to
9.ShapesandAnimation
143
A KeyValueisthebasicbuildingblockofJavaFXanimation.Itspecifiesapropertyandthe
"newvalue"itwillgraduallybetransitionedto.Soifyouhavea Rectanglewitha
rotateProperty()of 0.0,andyouspecifya KeyValuethatchangesitto 90.0degrees,it
willincrementallymovefrom 0.0to 90.0degrees.Putthat KeyValueinsidea KeyFrame
whichwillspecifyhowlongtheanimationbetweenthosetwovalueswilllast.Inthiscase
let'smakeit5seconds.Thenfinallyputthat KeyFrameina Timeline.Ifyourunthecode
below,youwillseearectangegraduallyrotatefrom`0.0`to`90.0`degreesin5seconds
(Figure9.1).
valrectangle=rectangle(width=60.0,height=40.0){
padding=Insets(20.0)
}
timeline{
keyframe(Duration.seconds(5.0)){
keyvalue(rectangle.rotateProperty(),90.0)
}
}
Figure9.1
Inagiven KeyFrame,youcansimultaneouslymanipulateotherpropertiesinthat5-second
windowtoo.Forinstancewecantransitionthe arcWidthProperty()and
arcHeightProperty()whilethe Rectangleisrotating(Figure9.2)
timeline{
keyframe(Duration.seconds(5.0)){
keyvalue(rectangle.rotateProperty(),90.0)
keyvalue(rectangle.arcWidthProperty(),60.0)
keyvalue(rectangle.arcHeightProperty(),60.0)
}
}
Figure9.2
9.ShapesandAnimation
144
Interpolators
Youcanalsospecifyan Interpolatorwhichcanaddsubtleeffectstotheanimation.For
instance,youcanspecify Interpolator.EASE_BOTHtoaccelerateanddeceleratethevalue
changeatthebeginningandendoftheanimationgracefully.
valrectangle=rectangle(width=60.0,height=40.0){
padding=Insets(20.0)
}
timeline{
keyframe(5.seconds){
keyvalue(rectangle.rotateProperty(),180.0,interpolator=Interpolator.EASE_B
OTH)
}
}
CyclesandAutoReverse
Youcanmodifyotherattributesofthe timeline()suchas cycleCountand autoReverse.
The cycleCountwillrepeattheanimationaspecifiednumberoftimes,andsettingthe
isAutoReversetotruewillcauseittorevertbackwitheachcycle.
timeline{
keyframe(5.seconds){
keyvalue(rectangle.rotateProperty(),180.0,interpolator=Interpolator.EASE_B
OTH)
}
isAutoReverse=true
cycleCount=3
}
Torepeattheanimationindefinitely,setthe cycleCountto Timeline.INDEFINITE.
ShorthandAnimation
9.ShapesandAnimation
145
Ifyouwanttoanimateasingleproperty,youcanquicklyanimateitwithoutdeclaringa
timeline(), keyframe(),and keyset().Callthe animate()extensionfunctiononthat
propertandprovidethe endValue,the duration,andoptionallythe interoplator.Thisis
muchshorterandcleanerifyouareanimatingjustoneproperty.
rectangle.rotateProperty().animate(endValue=180.0,duration=5.seconds)
Summary
Inthischapterwecoveredbuildersforshapeandanimation.WedidnotcoverJavaFX's
Canvasasthisisbeyondthescopeofthe TornadoFXframework.Itcouldeasilytakeup
morethanseveralchaptersonitsown.Buttheshapesandanimationshouldallowyoutodo
basiccustomgraphicsforamajorityoftasks.
ThisconcludesourcoverageofTornadoFXbuildersfornow.NextwewillcoverFXMLfor
thoseofusthathaveneedtouseit.
9.ShapesandAnimation
146
FXMLandInternationalization
TornadoFX'stype-safebuildersprovideafast,easy,anddeclarativewaytoconstructUI's.
ThisDSLapproachisencouragedbecauseitismoreflexible,reliable,andsimpler.
However,JavaFXalsosupportsanXML-basedstructurecalledFXMLthatcanalsobuilda
UIlayout.TornadoFXhastoolstostreamlineFXMLusageforthosethatneedit.
IfyouareunfamiliarwithFXMLandareperfectlyhappywithtype-safebuilders,pleasefeel
freetoskipthischapter.IfyouneedtoworkwithFXMLorfeelyoushouldlearnit,please
readon.YoucanalsotakealookattheofficialFXMLdocumentationtolearnmore.
ReasonsforConsideringFXML
WhilethedevelopersofTornadoFXstronglyencourageusingtype-safebuilders,thereare
situationsandfactorsthatmightcauseyoutoconsiderusingFXML.
SeparationofConcerns
WithFXMLitiseasytoseparateyourUIlogiccodefromtheUIlayoutcode.Thisseparation
isjustasachievablewithtype-safebuildersbyutilizingMVPorotherseparationpattern.But
someprogrammersfindFXMLforcesthemtomaintainthisseparationandpreferitforthat
reason.
WYSIWYGEditor
FXMLfilesalsocanbeeditedandprocessedbySceneBuilder,avisuallayouttoolthat
allowsbuildinginterfacesviadrag-and-dropfunctionality.EditsinSceneBuilderare
immediatelyrenderedinaWYSIWYG("WhatYouSeeisWhatYouGet")panenexttothe
editor.
Ifyouprefermakinginterfacesviadrag-and-drop,orhavetroublebuildingUI'swithpure
code,youmightconsiderusingFXMLsimplytoleverageSceneBuilder.
TheSceneBuildertoolwascreatedbyOracle/SunbutisnowmaintainedbyGluon,an
innovativecompanythatinvestsheavilyinJavaFXtechnology,especiallyforthemobile
market.
CompatibilitywithExistingCodebases
10.FXML
147
IfyouareconvertinganexistingJavaFXapplicationtoTornadoFX,thereisastrongchance
yourUIwasconstructedwithFXML.IfyouhesitatetotransitionlegacyFXMLtoTornadoFX
buidlers,orwouldliketoputthatoffaslongaspossible,TornadoFXcanatleaststreamline
theprocessingofFXML.
HowFXMLworks
The rootpropertyofa Viewrepresentsthetoplevel Nodecontainingahierarchyof
childrenNodes,whichmakesuptheuserinterface.WhenyouworkwithFXML,youdonot
instantiatethisrootnodedirectly,butinsteadaskTornadoFXtoloaditfromacorresponding
FXMLfile.Bydefault,TornadoFXwilllookforafilewiththesamenameasyourviewwith
the .fxmlfileendinginthesamepackageasyour Viewclass.Youcanalsooverridethe
FXMLlocationwithaparameterifyouwanttoputallyourFXMLfilesinasinglefolderor
organizethemsomeotherwaythatdoesnotdirectlycorrespondtoyour Viewlocation.
ASimpleExample
Let'screateabasicuserinterfacethatpresentsa Labelanda Button.Wewilladd
functionalitytothisviewsowhenthe Buttonisclicked,the Labelwillupdateits text
withthenumberoftimesthe Buttonhasbeenclicked.
Createafilenamed CounterView.fxmlwiththefollowingcontent:
10.FXML
148
<?importjavafx.geometry.Insets?>
<?importjavafx.scene.control.Button?>
<?importjavafx.scene.control.Label?>
<?importjavafx.scene.layout.BorderPane?>
<?importjavafx.scene.layout.VBox?>
<?importjavafx.scene.text.Font?>
<BorderPanexmlns="http://javafx.com/javafx/null"xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insetstop="20"right="20"bottom="20"left="20"/>
</padding>
<center>
<VBoxalignment="CENTER"spacing="10">
<Labeltext="0">
<font>
<Fontsize="20"/>
</font>
</Label>
<Buttontext="Clicktoincrement"/>
</VBox>
</center>
</BorderPane>
Youmaynoticeaboveyouhaveto importthetypesyouuseinFXMLjustlikecoding
inJavaorKotlin.IntellijIDEAshouldhaveaplugintosupportusingALT+ENTERto
generatethe importstatements.
IfyouloadthisfileinSceneBuilderyouwillseethefollowingresult(Figure9.1).
Figure9.1
Nextlet'sloadthisFXMLintoTornadoFX.
LoadingFXMLintoTornadoFX
10.FXML
149
WehavecreatedanFXMLfilecontainingourUIstructure,butnowweneedtoloaditintoa
TornadoFX Viewforittobeusable.Logically,wecanloadthis Nodehierarchyintothe
rootnodeofour View.Definethefollowing Viewclass:
classCounterView:View(){
overridevalroot:BorderPanebyfxml()
}
Notethatthe rootpropertyisdefinedbythe fxml()delegate.The fxml()delegate
takescareofloadingthecorresponding CounterView.fxmlintothe rootproperty.Ifwe
placed CounterView.fxmlinadifferentlocation(suchas /views/)thatisdifferentthan
wherethe CounterViewfileresides,wewouldaddaparameter.
classCounterView:View(){
overridevalroot:BorderPanebyfxml("/views/CounterView.fxml")
}
WehavelaidouttheUI,butithasnofunctionalityyet.Weneedtodefineavariablethat
holdsthenumberoftimesthe Buttonhasbeenclicked.Addavariablecalled counterand
defineafunctionthatwillincrementitsvalue:
classCounterView:View(){
overridevalroot:BorderPanebyfxml()
valcounter=SimpleIntegerProperty()
funincrement(){
counter.value+=1
}
}
Wewantthe increment()functiontobecalledwheneverthe Buttonisclicked.Backinthe
FXMLfile,addthe onActionattributetothebutton:
<Buttontext="Clicktoincrement"onAction="#increment"/>
SincetheFXMLfileautomaticallygetsboundtoour View,wecanreferencefunctionsvia
the #functionNamesyntax.Notethatwedonotaddparenthesistothefunctioncall,andyou
cannotpassparametersdirectly.Youcanhoweveraddaparameteroftype
javafx.event.ActionEventtothe incrementfunctionifyouwantinspectthesource Node
oftheactionorcheckwhatkindofactiontriggeredthebutton.Forthisexamplewedonot
needit,soweleavethe incrementfunctionwithoutparameters.
10.FXML
150
FXMLfilelocations
Bydefault,buildtoolslikeMavenandGradlewillignoreanyextraresourcesyouputinto
yoursourcerootfolders,soifyouputyourFXMLfilestheretheywon'tbeavailableat
runtimeunlessyouspecificallytellyourbuildtooltoincludethem.Thiscouldstillbe
problematicbecauseIDEAmightnotpickupyourcustomresourcelocationfromthebuild
file,onceagainresultinginfailureatruntime.Forthatresource,werecommendthatyou
placeyourFXMLfilesin src/main/resourcesandeitherfollowthesamefolderstructureas
yourpackages,orputthemallina viewsfolderorsimilar.Thelatterrequiresyoutoadd
theFXMLlocationparametertothe fxmldelegate,andmightbemessyifyouhavealarge
numberofViews,sogoingwiththedefaultisagoodidea.
AccessingNodeswiththe fxiddelegate
UsingjustFXML,wehavewiredthe Buttontocall increment()everytimeitiscalled.We
stillneedtobindthe countervaluetothe textpropertyofthe Label.Todothis,weneed
anidentifierforthe Label,soinourFXMLfileweaddthe fx:idattributetoit.
<Labelfx:id="counterLabel">
Nowwecaninjectthis Labelintoour Viewclass:
valcounterLabel:Labelbyfxid()
ThistellsTornadoFXtolookfora Nodeinourstructurewiththe fx:idpropertywiththe
samenameasthepropertywedefined(whichis"counterLabel").Itisalsopossibletouse
anotherpropertynameinthe Viewandaddanameparametertothe fxiddelegate:
valmyLabel:Labelbyfxid("counterLabel")
Nowthatwehaveaholdofthe Label,wecanusethebindingshortcutsofTornadoFXto
bindthe countervaluetothe textpropertyofthe counterLabel.Ourwhole Viewshould
nowlooklikethis:
10.FXML
151
classCounterView:View(){
overridevalroot:BorderPanebyfxml()
valcounter=SimpleIntegerProperty()
valcounterLabel:Labelbyfxid()
init{
counterLabel.bind(counter)
}
funincrement(){
counter.value+=1
}
}
Ourappisnowcomplete.Everytimethebuttonisclicked,the labelwillincrementits
count.
Internationalization
JavaFXhasstrongsupportformulti-languageUI's.TosupportinternationalizationinFXML,
younormallyhavetoregisteraresourcebundlewiththe FXMLLoaderanditwillinreturn
replaceinstancesofresourcenameswiththeirlocale-specificvalue.Aresourcenameisthe
keyintheresourcebundleprependedwith %.
TornadoFXmakesthiseasierbysupportingaconventionforresourcebundles:Createa
resourcebundlewiththesamebasenameasyour View,anditwillbeautomatically
loaded,bothforuseprogramaticallywithinthe ViewandfromtheFXMLfile.
Let'sinternationalizethebuttontextinourUI.Createafilecalled CounterView.properties
andaddthefollowingcontent:
clickToIncrement=Clicktoincrement
Ifyouwanttosupportmultiplelanguages,createafilewiththesamebasenamefollowedby
anunderscore,andthenthelanguagecode.Forinstance,tosupportFrenchcreatethefile
CounterView_fr.properties.Theclosestgeographicalmatchtothecurrentlocalewillbe
used.
clickToIncrement=Cliquezsurincrément
NowweswapthebuttontextwiththeresourcekeyintheFXMLfile.
10.FXML
152
<Buttontext="%clickToIncrement"onAction="#increment"/>
Ifyouwanttotestthisfunctionalityandforceadifferent Locale,regardlesswhichoneyou
arecurrentlyin,overrideitbyassigning FX.localwhenyour Appclassisinitialized.
classMyApp:App(){
overridevalprimaryView=MyView::class
init{
FX.locale=Locale.FRENCH
}
}
Youshouldthenseeyour ButtonusetheFrenchtext(Figure9.2).
Figure9.2
InternationalizationwithType-SafeBuilders
InternationalizationisnotlimitedforusewithFXML.Youcanalsouseitwithtype-safe
builders.Setupyour .propertiesfilesasspecifiedbefore.Butinsteadofusingan
embedded %clickToIncrementtextinanFXMLfile,usethe messages[]accessortolookup
thevalueinthe ResourceBundle.Passthisvalueasthe textforthe Button.
button(messages["clickToIncrement"]){
setOnAction{increment()}
}
Summary
10.FXML
153
FXMLishelpfultoknowasaJavaFXdeveloper,butitisdefinitelynotrequiredifyouare
contentwithTornadoFXtype-safebuildersanddonothaveanyexistingJavaFXapplications
tomaintain.Type-safebuildershavethebenefitofusingpureKotlin,allowingyoutocode
anythingyouwantrightwithinthestructuredeclarations.FXML'sbenefitsareprimarily
separationofconcernsbetweenUIandfunctionality,buteventhatcanbeaccomplishedwith
type-safebuilders.Italsocanbebuiltviadrag-and-dropthroughtheSceneBuildertool,
whichmaybepreferableforthosewhostruggletobuildUI'sanyotherway.
10.FXML
154
EditingModelsandValidation
TornadoFXdoesn'tforceanyparticulararchitecturalpatternonyouasadeveloper,andit
worksequallywellwithbothMVC,MVP,andtheirderivatives.
TohelpwithimplementingthesepatternsTornadoFXprovidesatoolcalled ViewModelthat
helpscleanlyseparateyourUIandbusinesslogic,givingyoufeatureslikerollback/commit
anddirtystatechecking.Thesepatternsarehardorcumbersometoimplementmanually,so
itisadvisedtoleveragethe ViewModeland ViewModelItemwhenitisneeded.
Typicallyyouwillusethe ItemViewModelwhenyouarecreatingafacadeinfrontofasingle
object,anda ViewModelformorecomplexsituations.
ATypicalUseCase
Sayyouhaveagivendomaintype Person.Weallowitstwopropertiestobenullableso
theycanbeinputtedlaterbytheuser.
importtornadofx.*
classPerson(name:String?=null,title:String?=null){
valnameProperty=SimpleStringProperty(this,"name",name)
varnamebynameProperty
valtitleProperty=SimpleStringProperty(this,"title",title)
vartitlebytitleProperty
}
(Noticetheimport,youneedtoimportatleast tornadofx.getValueand tornadofx.setValue
forthebydelegatetowork)*[]:
ConsideraMaster/Detailviewwhereyouhavea TableViewdisplayingalistofpeople,and
a Formwherethecurrentlyselectedperson'sinformationcanbeedited.Beforewegetinto
the ViewModel,wewillcreateaversionofthis Viewwithoutusingthe ViewModel.
11.EditingModelsandValidation
155
Figure11.1
Belowiscodeforourfirstattemptinbuildingthis,andithasanumberofproblemswewill
address.
importjavafx.scene.control.TableView
importjavafx.scene.control.TextField
importjavafx.scene.layout.BorderPane
importtornadofx.*
classPerson(name:String?=null,title:String?=null){
valnameProperty=SimpleStringProperty(this,"name",name)
varnamebynameProperty
valtitleProperty=SimpleStringProperty(this,"title",title)
vartitlebytitleProperty
}
classPersonEditor:View("PersonEditor"){
overridevalroot=BorderPane()
varnameField:TextFieldbysingleAssign()
vartitleField:TextFieldbysingleAssign()
varpersonTable:TableView<Person>bysingleAssign()
//Somefakedataforourtable
valpersons=listOf(Person("John","Manager"),Person("Jay","Workerbee")).obser
11.EditingModelsandValidation
156
vable()
varprevSelection:Person?=null
init{
with(root){
//TableViewshowingalistofpeople
center{
tableview(persons){
personTable=this
column("Name",Person::nameProperty)
column("Title",Person::titleProperty)
//Editthecurrentlyselectedperson
selectionModel.selectedItemProperty().onChange{
editPerson(it)
prevSelection=it
}
}
}
right{
form{
fieldset("Editperson"){
field("Name"){
textfield(){
nameField=this
}
}
field("Title"){
textfield(){
titleField=this
}
}
button("Save").action{
save()
}
}
}
}
}
}
privatefuneditPerson(person:Person?){
if(person!=null){
prevSelection?.apply{
nameProperty.unbindBidirectional(nameField.textProperty)
titleProperty.unbindBidirectional(titleField.textProperty)
}
nameField.bind(person.nameProperty)
titleField.bind(person.titleProperty)
prevSelection=person
}
11.EditingModelsandValidation
157
}
privatefunsave(){
//ExtracttheselectedpersonfromthetableView
valperson=personTable.selectedItem!!
//Arealapplicationwouldpersistthepersonhere
println("Saving${person.name}/${person.title}")
}
}
Wedefinea Viewconsistingofa TableViewinthecenterofa BorderPaneanda Formon
therightside.Wedefinesomepropertiesfortheformfieldsandthetableitselfsowecan
referencethemlater.
Whilewebuildthetable,weattachalistenertotheselecteditemsowecancallthe
editPersonfunctionwhenthetableselectionchanges.The editPersonfunctionbindsthe
propertiesoftheselectedpersontothetextfieldsintheform.
Problemswithourinitialattempt
AtfirstglanceitmightlookOK,butwhenwedigdeeperthereareseveralissues.
Manualbinding
Everytimetheselectioninthetablechanges,wehavetounbind/rebindthedatafortheform
fieldsmanually.Apartfromtheaddedcodeandlogic,thereisanotherhugeproblemwith
this:thedataisupdatedforeverychangeinthetextfields,andthechangeswillevenbe
reflectedinthetable.Whilethismightlookcoolandistechnicallycorrect,itpresentsonebig
problem:whatiftheuserdoesnotwanttosavethechanges?Wehavenowayofrolling
back.Sotopreventthis,wewouldhavetoskipthebindingaltogetherandmanuallyextract
thevaluesfromthetextfields,thencreateanew Personobjectonsave.Infact,thisisa
patternfoundinmanyapplicationsandexpectedbymostusers.Implementinga"Reset"
buttonforthisformwouldmeanmanagingvariableswiththeinitialvaluesandagain
assigningthosevaluesmanuallytothetextfields.
TightCoupling
Anotherissueiswhenitistimetosavetheeditedperson,thesavefunctionhastoextract
theselecteditemfromthetableagain.Forthattohappenthesavefunctionhastoknow
aboutthe TableView.Alternativelyitwouldhavetoknowaboutthetextfieldslikethe
editPersonfunctiondoes,andmanuallyextractthevaluestoreconstructa Personobject.
11.EditingModelsandValidation
158
IntroducingViewModel
The ViewModelisamediatorbetweenthe TableViewandthe Form.Itactsasa
middlemanbetweenthedatainthetextfieldsandthedataintheactual Personobject.As
youwillsee,thecodeismuchshorterandeasiertoreasonabout.Theimplementationcode
ofthe PersonModelwillbeshownshortly.Fornowjustfocusonitsusage.
classPersonEditor:View("PersonEditor"){
overridevalroot=BorderPane()
valpersons=listOf(Person("John","Manager"),Person("Jay","Workerbee")).obser
vable()
valmodel=PersonModel(Person())
init{
with(root){
center{
tableview(persons){
column("Name",Person::nameProperty)
column("Title",Person::titleProperty)
//Updatethepersoninsidetheviewmodelonselectionchange
model.rebindOnChange(this){selectedPerson->
person=selectedPerson?:Person()
}
}
}
right{
form{
fieldset("Editperson"){
field("Name"){
textfield(model.name)
}
field("Title"){
textfield(model.title)
}
button("Save"){
enableWhen(model.dirty)
action{
save()
}
}
button("Reset").action{
model.rollback()
}
}
}
}
}
}
11.EditingModelsandValidation
159
privatefunsave(){
//Flushchangesfromthetextfieldsintothemodel
model.commit()
//Theeditedpersoniscontainedinthemodel
valperson=model.person
//Arealapplicationwouldpersistthepersonhere
println("Saving${person.name}/${person.title}")
}
}
classPersonModel(varperson:Person):ViewModel(){
valname=bind{person.nameProperty}
valtitle=bind{person.titleProperty}
}
Thislooksalotbetter,butwhatexactlyisgoingonhere?Wehaveintroducedasubclassof
ViewModelcalled PersonModel.Themodelholdsa Personobjectandhaspropertiesfor
the nameand titlefields.Wewilldiscussthemodelfurtherafterwehavelookedatthe
restoftheclientcode.
Notethatweholdnoreferencetothe TableVieworthetextfields.Apartfromalotless
code,thefirstbigchangeisthewayweupdatethe Personinsidethemodel:
model.rebindOnChange(this){selectedPerson->
person=selectedPerson?:Person()
}
The rebindOnChange()functiontakesthe TableViewasanargumentandafunctionthatwill
becalledwhentheselectionchanges.Thisworkswith ListView,TreeView,
TreeTableView,andanyother ObservableValueaswell.Thisfunctioniscalledonthemodel
andhasthe selectedPersonasitssingleargument.Weassigntheselectedpersontothe
personpropertyofthemodel,oranew Personiftheselectionwasempty/null.Thatway
weensurethatthereisalwaysdataforthemodeltopresent.
WhenwecreatetheTextFields,webindthemodelpropertiesdirectlytoitsincemost Node
buildersacceptan ObservableValuetobindto.
field("Name"){
textfield(model.name)
}
Evenwhentheselectionchanges,themodelpropertiespersistbutthevaluesforthe
propertiesareupdated.Wetotallyavoidthemanualbindingfromourpreviousattempt.
11.EditingModelsandValidation
160
Anotherbigchangeinthisversionisthatthedatainthetabledoesnotupdatewhenwetype
intothetextfields.Thisisbecausethemodelhasexposedacopyofthepropertiesfromthe
personobjectanddoesnotwritebackintotheactualpersonobjectbeforewecall
model.commit().Thisisexactlywhatwedointhe savefunction.Once commithasbeen
called,thedatainthefacadeisflushedbackintoourpersonobjectandthetablewillnow
reflectourchanges.
Rollback
Sincethemodelholdsareferencetotheactual Personobject,wecancanresetthetext
fieldstoreflecttheactualdatainour Personobject.Wecouldaddaresetbuttonlikethis:
button("Reset").action{
model.rollback()
}
Whenthebuttonispressed,anychangesarediscardedandthetextfieldsshowtheactual
Personobjectvaluesagain.
ThePersonModel
Weneverexplainedhowthe PersonModelworksyet,andyouprobablyhavebeen
wonderingabouthowthe PersonModelisimplemented.Hereitis:
classPersonModel(varperson:Person):ViewModel(){
valname=bind{person.nameProperty}
valtitle=bind{person.titleProperty}
}
Itcanholda Personobject,andithasdefinedtwostrange-lookingpropertiescalled name
and titleviathe binddelegate.Yeahitlooksweird,butthereisaverygoodreasonfor
it.The {person.nameProperty}parameterforthe bindfunctionisalambdathatreturnsa
property.Thisreturnedpropertyisexaminedbythe ViewModel,andanewpropertyofthe
sametypeiscreated.Itisthenputintothe namepropertyofthe ViewModel.
Whenwebindatextfieldtothe namepropertyofthemodel,onlythecopyisupdatedwhen
youtypeintothetextfield.The ViewModelkeepstrackofwhichactualpropertybelongsto
whichfacade,andwhenyoucall committhevaluesfromthefacadeareflushedintothe
actualbackingproperty.Ontheflipside,whenyoucall rollbacktheexactopposite
happens:Theactualpropertyvalueisflushedintothefacade.
11.EditingModelsandValidation
161
Thereasontheactualpropertyiswrappedinafunctionisthatthismakesitpossibleto
changethe personvariableandthenextractthepropertyfromthatnewperson.Youcan
readmoreaboutthisbelow(rebinding).
DirtyChecking
Themodelhasa Propertycalled dirty.Thisisa BooleanBindingwhichyoucanobserve
toenableordisablecertainfeatures.Forexample,wecouldeasilydisablethesavebutton
untilthereareactualchanges.Theupdatedsavebuttonwouldlooklikethis:
button("Save"){
enableWhen(model.dirty)
action{
save()
}
}
Thereisalsoaplain valcalled isDirtywhichreturnsa Booleanrepresentingthedirty
statefortheentiremodel.
Onethingtonoteisthatifthebackingobjectisbeingmodifiedwhilethe ViewModelisalso
modifiedviatheUI,alluncommittedchangesinthe ViewModelarebeingoverriddenbythe
changesinthebackingobject.Thatmeansthedatainthe ViewModelmightgetlostif
externalmodificationofthebackingobjecttakesplace.
valperson=Person("John","Manager")
valmodel=PersonModel(person)
model.name.value="Johnny"//modifytheViewModel
person.name="Johan"//modifytheunderlyingobject
println("Person=${person.name},${person.title}")//output:Person=
Johan,Manager
println("Isdirty=${model.isDirty}")//output:Isdirty=
false
println("Model=${model.name.value},${model.title.value}")//output:Model=
Johan,Manager
Ascanbeseenabovethechangesinthe ViewModelgotoverriddenwhentheunderlying
objectwasmodified.Andthe ViewModelwasnotflaggedas dirty.
DirtyProperties
11.EditingModelsandValidation
162
Youcancheckifaspecificpropertyisdirty,meaningthatithasbeenchangedcomparedto
thebackingsourceobjectvalue.
valnameWasChanged=model.isDirty(model.name)
Thereisalsoanextensionpropertyversionthataccomplishesthesametask:
valnameWasChanged=model.name.isDirty
Theshorthandversionisanextension valon Property<T>butitwillonlyworkfor
propertiesthatareboundinsidea ViewModel.Youwillfind model.isNotDirtypropertiesas
well.
Ifyouneedtodynamicallyreactbasedonthedirtystateofaspecificpropertyinthe
ViewModel,youcangetaholdofa BooleanBindingrepresentingthedirtystateofthatfield
likethis:
valnameDirtyProperty=model.dirtyStateFor(PersonModel::name)
ExtractingtheSourceObjectValue
Toretrievethebackingobjectvalueforapropertyyoucancall
model.backingValue(property).
valperson=model.backingValue(property)
SupportingObjectsthatDoNotExposeJavaFX
Properties
YouprobablywonderedhowtodealwithdomainobjectsthatdonotuseJavaFXproperties.
MaybeyouhaveasimplePOJOwithgettersandsetters,ornormalkotlin vartype
properties.Since ViewModelrequiresJavaFXproperties,TornadoFXcomeswithpowerful
wrappersthatcanturnanytypeofpropertyintoanobservableJavaFXproperty.Hereare
someexamples:
11.EditingModelsandValidation
163
//JavaPOJOgetter/setterproperty
classJavaPersonViewModel(person:JavaPerson):ViewModel(){
valname=bind{person.observable(JavaPerson::getName,JavaPerson::setName)}
}
//Kotlinvarproperty
classPersonVarViewModel(person:Person):ViewModel(){
valname=bind{person.observable(Person::name)}
}
Asyoucansee,itiseasytoconvertanypropertytypetoanobservableproperty.
SpecificPropertySubtypes(IntegerProperty,
BooleanProperty)
Ifyoubind,forexample,an IntegerProperty,thetypeofthefacadepropertywilllooklike
Property<Int>butitisinfactan IntegerPropertyunderthehood.Ifyouneedtoaccessthe
specialfunctionsprovidedby IntegerProperty,youwillhavetocastthebindresult:
valage=bind(Person::ageProperty)asIntegerProperty
Similarily,youcanexposeareadonlypropertybyspecifyingareadonlytype:
valage=bind(Person::ageProperty)asReadOnlyIntegerProperty
Thereasonforthisisanunfortunateshortcomingonthetypesystemthatpreventsthe
compilerfromdifferentiatingbetweenoverloaded bindfunctionsforthesespecifictypes,so
thesingle bindfunctioninside ViewModelinspectsthepropertytypeandreturnsthebest
match,butunfortunatelythereturntypesignaturehastobe Property<T>fornow.
Rebinding
Asyousawinthe TableViewexampleabove,itispossibletochangethedomainobjectthat
iswrappedbythe ViewModel.Thistestcaseshedssomemorelightonthat:
11.EditingModelsandValidation
164
@Testfunswap_source_object(){
valperson1=Person("Person1")
valperson2=Person("Person2")
valmodel=PersonModel(person1)
assertEquals(model.name,"Person1")
model.rebind{person=person2}
assertEquals(model.name,"Person2")
}
Thetestcreatestwo Personobjectsanda ViewModel.Themodelisinitialisedwiththefirst
personobject.Itthenchecksthat model.namecorrespondstothenamein person1.Now
somethingweirdhappens:
model.rebind{person=person2}
Thecodeinsidethe rebind()blockabovewillbeexecutedandallthepropertiesofthe
modelareupdatedwithvaluesfromthenewsourceobject.Thisisactuallyanalogousto
writing:
model.person=person2
model.rebind()
Theformyouchooseisuptoyou,butthefirstformmakessureyoudonotforgettocall
rebind.After rebindiscalled,themodelisnotdirtyandallvalueswillreflecttheonesform
thenewsourceobjectorsourceobjects.It'simportanttonotethatyoucanpassmultiple
sourceobjectstoaviewmodelandupdateallorsomeofthemasyouseefit.
RebindListener
Our TableViewexamplecalledthe rebindOnChange()functionandpassedina TableView
asthefirstargument.Thismadesurethatrebindwascalledwhenevertheselectionofthe
TableViewchanged.Thisisactuallyjustashortcuttoanotherfunctionwiththesamename
thattakesanobservableandcallsrebindwheneverthatobservablechanges.Ifyoucallthis
function,youdonotneedtocallrebindmanuallyaslongasyouhaveanobservablethat
representthestatechangethatshouldcausethemodeltorebind.
Asyousaw, TableViewhasashorthandsupportforthe
selectionModel.selectedItemProperty.Ifnotforthisshorthandfunctioncall,youwouldhave
towriteitlikethis:
11.EditingModelsandValidation
165
model.rebindOnChange(table.selectionModel.selectedItemProperty()){
person=it?:Person()
}
Theaboveexampleisincludedtoclarifyhowthe rebindOnChange()functionworksunder
thehood.Forrealusecasesinvolvinga TableView,youshouldoptfortheshorterversion
orusethe ItemViewModel.
ItemViewModel
Whenworkingwiththe ViewModelyouwillnoticesomerepetitiveandsomewhatverbose
tasks.Theyincludecalling rebindorconfiguring rebindOnChangetochange
thesourceobject.The ItemViewModelisanextensiontothe ViewModelandinalmostall
usecasesyouwouldwanttoinheritfromthisinsteadofthe ViewModelclass.
The ItemViewModelhasapropertycalled itemPropertyofthespecifiedtype,soour
PersonModelwouldnowlooklike:
classPersonModel:ItemViewModel<Person>(){
valname=bind(Person::nameProperty)
valtitle=bind(Person::titleProperty)
}
Youwillnoticewenolongerneedtopassinthe varperson:Personintheconstructor.The
ItemViewModelnowhasanobservablepropertycalled
itemPropertyandgetters/settersviathe itemproperty.Wheneveryouassignsomething
to itemorvia itemProperty.value,themodelis
automaticallyreboundforyou.Thereisalsoanobservable emptybooleanvalueyoucan
usetocheckifthe ItemViewModeliscurrentlyholdinga Person.
Thebindingexpressionsneedtotakeintoaccountthatitmightnotrepresentanyitematthe
timeofbinding.Thatiswhythebinding
expressionsabovenowusethenullsafeoperator.
Wejustgotridofsomeboilerplate,butthe ItemViewModelgivesusalotmorefunctionality.
Rememberhowweboundtheselectedpersonfromthe TableViewtoourmodelearlier?
//Updatethepersoninsidetheviewmodelonselectionchange
model.rebindOnChange(this){selectedPerson->
person=selectedPerson?:Person()
}
11.EditingModelsandValidation
166
Usingthe ItemViewModelthiscanberewritten:
//Updatethepersoninsidetheviewmodelonselectionchange
bindSelected(model)
Thiswilleffectivelyattachthelistenerwehadtowritemanuallybeforeandmakesurethat
the TableViewselectionisvisibleinthemodel.
The save()functionwillnowalsobeslightlydifferent,sincethereisno personpropertyin
ourmodel:
privatefunsave(){
model.commit()
valperson=model.item
println("Saving${person.name}/${person.title}")
}
Thepersonisextractedfromthe itemPropertyusingthe itemgetter.
WhenworkingwiththeItemViewModel()andPOJO'sstartingat1.7.1youcancreatethe
bindingsasfollows
dataclassPerson(valfirstName:String,vallastName:String)
classPersonModel:ItemViewModel<Person>(){
valfirstname=bind{item?.firstName?.toProperty()}
vallastName=bind{item?.lastName?.toProperty()}
}
OnCommitcallback
Sometimesit'sdesirabletodoaspecificactionafterthemodelwassuccessfullycommitted.
The ViewModelofferstwocallbacks, onCommitand onCommit(commits:List<Commit>),for
that.
Thefirstfunction onCommit,hasnoparametersandwillbecalledafterasuccessfulcommit,
rightbeforetheoptional successFnisinvoked(see: commit).
Thesecondfunctionwillbecalledinthesameorderandwiththeadditionofpassingalistof
committedpropertiesalong.
Each Commitinthelist,consistsoftheoriginal ObservableValue,the oldValueandthe
newValue
andaproperty changed,tosignalifthe oldValueisdifferentthenthe newValue.
11.EditingModelsandValidation
167
Let'slookatanexamplehowwecanretrieveonlythechangedobjectsandprintthemto
stdout.
Tofindoutwhichobjectchangedwedefinedalittleextensionfunction,whichwillfindthe
givenpropertyand
ifitwaschangedwillreturntheoldandnewvalueornulliftherewasnochange.
classPersonModel:ItemViewModel<Person>(){
valfirstname=bind(Person::firstName)
vallastName=bind(Person::lastName)
overridevalonCommit(commits:List<Commit>){
//TheprintlnwillonlybecallediffindChangedisnotnull
commits.findChanged(firstName)?.let{println("First-Namechangedfrom${it.fir
st}to${it.second}")}
commits.findChanged(lastName)?.let{println("Last-Namechangedfrom${it.first
}to${it.second}")}
}
privatefun<T>List<Commit>.findChanged(ref:Property<T>):Pair<T,T>?{
valcommit=find{it.property==ref&&it.changed}
returncommit?.let{(it.newValueasT)to(it.oldValueasT)}
}
}
InjectableModels
Mostcommonlyyouwillnothaveboththe TableViewandtheeditorinthesame View.We
wouldthenneedtoaccessthe ViewModelfromatleasttwodifferentviews,oneforthe
TableViewandonefortheform.Luckily,the ViewModelisinjectable,sowecanrewriteour
editorexampleandsplitthetwoviews:
classPersonList:View("PersonList"){
valpersons=listOf(Person("John","Manager"),Person("Jay","Workerbee")).obser
vable()
valmodel:PersonModelbyinject()
overridevalroot=tableview(persons){
title="Person"
column("Name",Person::nameProperty)
column("Title",Person::titleProperty)
bindSelected(model)
}
}
11.EditingModelsandValidation
168
Theperson TableViewnowbecomesalotcleanerandeasiertoreasonwith.Inareal
applicationthelistofpersonswouldprobablycomefromacontrolleroraremotingcall
though.Themodelissimplyinjectedintothe View,andwewilldothesamefortheeditor:
classPersonEditor:View("PersonEditor"){
valmodel:PersonModelbyinject()
overridevalroot=form{
fieldset("Editperson"){
field("Name"){
textfield(model.name)
}
field("Title"){
textfield(model.title)
}
button("Save"){
enableWhen(model.dirty)
action{
save()
}
}
button("Reset").action{
model.rollback()
}
}
}
privatefunsave(){
model.commit()
println("Saving${model.item.name}/${model.item.title}")
}
}
Theinjectedinstanceofthemodelwillbetheexactsameoneinbothviews.Again,inareal
applicationthesavecallwouldprobablybeoffloaded
toacontrollerasynchronously.
WhentoUse ViewModelvs ItemViewModel
Thischapterhasprogressedfromthelow-levelimplementation ViewModelintoa
streamlined ItemViewModel.Youmightwonderifthereareanyusecasesforinheritingfrom
ViewModelinsteadof ItemViewModelatall.Theansweristhatwhileyouwouldtypically
extend ItemViewModelmorethan90%ofthetime,therearesomeusecaseswhereitdoes
notmakesense.SinceViewModelscanbeinjectedandusedtokeepnavigationalstateand
overallUIstate,youmightuseitforsituationswhereyoudonothaveasingledomainobject
11.EditingModelsandValidation
169
-youcouldhavemultipledomainobjectsorjustacollectionoflooseproperties.Inthisuse
casethe ItemViewModeldoesnotmakeanysense,andyoumightimplementthe
ViewModeldirectly.Forcommoncasesthough, ItemViewModelisyourbestfriend.
Thereisonepotentialissuewiththisapproach.Ifwewanttodisplaymultiple"pairs"of
listsandforms,perhapsindifferentwindows,weneedawaytoseparateandbindthe
modelbelongingtoaspesificpairoflistandform.Therearemanywaystodealwith
that,butonetoolverywellsuitedforthisisthescopes.Checkoutthescope
documentationformoreinformationaboutthisapproach.
Validation
Almosteveryapplicationneedstocheckthattheinputsuppliedbytheuserconformstoa
setofrulesorareotherwiseacceptable.TornadoFXsportsanextensiblevalidationand
decorationframework.
Wewillfirstlookatvalidationasastandalonefeaturebeforeweintegrateitwiththe
ViewModel.
UndertheHood
Thefollowingexplanationisabitverboseanddoesnotreflectthewayyouwouldwrite
validationcodeinyourapplication.Thissectionwillprovideyouwithasolidunderstandingof
howvalidationworksandhowtheindividualpiecesfittogether.
Validator
A Validatorknowshowtoinspectuserinputofaspecifiedtypeandwillreturna
ValidationMessagewitha ValidationSeveritydescribinghowtheinputcomparestothe
expectedinputforaspecificcontrol.Ifa Validatordeemsthatthereisnothingtoreportfor
aninputvalue,itreturns null.Atextmessagecanoptionallyaccompanythe
ValidationMessage,andwouldnormallybedisplayedbythe Decoratorconfiguredinthe
ValidationContext.Wewillcovermoreondecoratorslater.
Thefollowingseveritylevelsaresupported:
Error-Inputwasnotaccepted
Warning-Inputisnotideal,butaccepted
Success-Inputisaccepted
Info-Inputisaccepted
11.EditingModelsandValidation
170
Therearemultipleseveritylevelsrepresentingsuccessfulinputtoeasierprovidethe
contextuallycorrectfeedbackinmostcases.Forexample,youmightwanttogivean
informationalmessageforafieldnomattertheinputvalue,orspecificallymarkfieldswitha
greencheckboxwhentheyareentered.Theonlyseveritythatwillresultinaninvalidstatus
isthe Errorlevel.
ValidationTrigger
Bydefaultvalidationwillhappenwhentheinputvaluechanges.Theinputvalueisalwaysan
ObservableValue<T>,andthedefaulttriggersimplylistensforchanges.Youcanhowever
choose
tovalidatewhentheinputfieldloosesfocus,orwhenasavebuttonisclickedforinstance.
ThefollowingValidationTriggerscanbeconfiguredforeachvalidator:
OnChange-Validatewheninputvaluechanges,optionallyafteragivendelayin
milliseconds
OnBlur-Validatewhentheinputfieldloosesfocus
Never-Onlyvalidatewhen ValidationContext.validate()iscalled
ValidationContext
Normallyyouwouldvalidateuserinputfrommultiplecontrolsorinputfieldsatonce.Youcan
gatherthesevalidatorsina ValidationContextsoyoucancheckifallvalidatorsarevalid,or
askthevalidationcontexttoperformvalidationforallfieldsatanygiventime.Thecontext
alsocontrolswhatkindofdecoratorwillbeusedtoconveythevalidationmessageforeach
field.SeetheAdHocvalidationexamplebelow.
Decorator
The decorationProviderofa ValidationContextisinchargeofprovidingfeedbackwhena
ValidationMessageisassociatedwithaninput.Bydefaultthisisaninstanceof
SimpleMessageDecoratorwhichwillmarktheinputfieldwithacoloredtriangleinthetopper
leftcorneranddisplayapopupwiththemessagewhiletheinputhasfocus.
11.EditingModelsandValidation
171
Figure11.2Thedefaultdecoratorshowingarequiredfieldvalidationmessage
Ifyoudon'tlikethedefaultdecoratorlookyoucaneasilycreateyourownbyimplementing
the Decoratorinterface:
interfaceDecorator{
fundecorate(node:Node)
funundecorate(node:Node)
}
Youcanassignyourdecoratortoagiven ValidationContextlikethis:
context.decorationProvider=MyDecorator()
Tip:YoucancreateadecoratorthatappliesCSSstyleclassestoyourinputsinstead
ofoverlayingothernodestoprovidefeedback.
AdHocValidation
11.EditingModelsandValidation
172
Whileyouwillprobablyneverdothisinarealapplication,itispossibletosetupa
ValidationContextandapplyvalidatorstoitmanually.Thefollowing
exampleisactuallytakenfromtheinternaltestsoftheframework.Itillustratestheconcept,
butisnotapracticalpatterninanapplication.
//Createavalidationcontext
valcontext=ValidationContext()
//CreateaTextFieldwecanattachvalidationto
valinput=TextField()
//Defineavalidatorthatacceptsinputlongerthan5chars
valvalidator=context.addValidator(input,input.textProperty()){
if(it!!.length<5)error("Tooshort")elsenull
}
//Simulateuserinput
input.text="abc"
//Validationshouldfail
assertFalse(validator.validate())
//Extractthevalidationresult
valresult=validator.result
//Theseverityshouldbeerror
assertTrue(resultisValidationMessage&&result.severity==ValidationSeverity.Error)
//Confirmvalidinputpassesvalidation
input.text="longvalue"
assertTrue(validator.validate())
assertNull(validator.result)
Takespecialnoteofthelastparametertothe addValidatorcall.Thisistheactualvalidation
logic.Thefunctionispassedthe
currentinputforthepropertyitvalidatesandmustreturnnulliftherearenomessages,oran
instanceof ValidationMessageifsomething
isnoteworthyabouttheinput.Amessagewithseverity Errorwillcausethevalidationto
fail.Asyoucansee,youdon'tneedtoinstantiate
aValidationMessageyourself,simplyuseoneofthefunctions error, warning, success
or infoinstead.
ValidationwithViewModel
11.EditingModelsandValidation
173
EveryViewModelcontainsa ValidationContext,soyoudon'tneedtoinstantiateone
yourself.TheValidationframeworkintegrateswiththetype
safebuildersaswell,andevenprovidessomebuiltinvalidators,likethe requiredvalidator.
Goingbacktoourpersoneditor,wecan
maketheinputfieldsrequiredwiththissimplechange:
field("Name"){
textfield(model.name).required()
}
That'sallthereistoit.Therequiredvalidatoroptionallytakesamessagethatwillbe
presentedtotheuserifthevalidationfails.Thedefaulttext
is"Thisfieldisrequired".
Insteadofusingthebuiltin requiredvalidatorwecanexpressthesamethingmanually:
field("Name"){
textfield(model.name).validator{
if(it.isNullOrBlank())error("Thenamefieldisrequired")elsenull
}
}
Ifyouwanttofurthercustomizethetextfield,youmightwanttoaddanothersetofcurly
braces:
field("Name"){
textfield(model.name){
//Manipulatethetextfieldhere
validator{
if(it.isNullOrBlank())error("Thenamefieldisrequired")elsenull
}
}
}
Bindingbuttonstovalidationstate
Youmightwanttoonlyenablecertainbuttonsinyourformswhentheinputisvalid.The
model.validpropertycanbeusedforthispurpose.Since
thedefaultvalidationtriggeris OnChange,thevalidstatewouldonlybeaccuratewhenyou
firsttrytocommitthemodel.However,ifyouwant
11.EditingModelsandValidation
174
tobindabuttontothe validstateofthemodelyoucancall model.validate(decorateErrors
=false)toforceallvalidatorstoreporttheirresultswithout
actuallyshowinganyvalidationerrorstotheuser.
field("username"){
textfield(username).required()
}
field("password"){
passwordfield(password).required()
}
buttonbar{
button("Login",ButtonBar.ButtonData.OK_DONE).action{
enableWhen{model.valid}
model.commit{
doLogin()
}
}
}
//Forcevalidatorstoupdatethe`model.valid`property
model.validate(decorateErrors=false)
Noticehowtheloginbutton'senabledstateisboundtotheenabledstateofthemodelvia
enableWhen{model.valid}call.Afterall
thefieldsandvalidatorsareconfigured,the model.validate(decorateErrors=false)make
surethevalidstateofthemodelisupdated
withouttriggeringerrordecorationsonthefieldsthatfailvalidation.Thedecoratorswillkick
inonvaluechangebydefault,unlessyou
overridethe triggerparameterto validator.The required()buildinvalidatoralso
acceptsthisparameter.Forexample,torunthevalidator
onlywhentheinputfieldloosesfocusyoucancall
textfield(username).required(ValidationTrigger.OnBlur).
Validationindialogs
The dialogbuildercreatesawindowwithaformandafieldsetandlet'syoustartadding
fieldstoit.Sometimesyoudon'thaveaViewModelforsuchcases,butyoumightstillwant
to
usethefeaturesitprovides.ForsuchsituationsyoucaninstantiateaViewModelinlineand
hookuponeormorepropertiestoit.Hereisanexampledialogthatrequirestheuserto
entersomeinputinatextarea:
11.EditingModelsandValidation
175
dialog("Addnote"){
valmodel=ViewModel()
valnote=model.bind{SimpleStringProperty()}
field("Note"){
textarea(note){
required()
whenDocked{requestFocus()}
}
}
buttonbar{
button("Savenote").action{
model.commit{doSave()}
}
}
}
Figure11.3AdialogwithainlineViewModelcontext
Noticehowthe notepropertyisconnectedtothecontextbyspecifyingit'sbeanparameter.
Thisiscrucialformakingthefieldvalidationavailable.
Partialcommit
It'salsopossibletodoapartialcommitbysupplingalistoffieldsyouwanttocommitto
avoidcommittingeverything.Thiscanbeconvenientinsituationswhereyoueditthesame
ViewModelinstancefromdifferentViews,forexampleinaWizard.SeetheWizardchapter
11.EditingModelsandValidation
176
formoreinformationaboutpartialcommit,andthecorrespondingpartialvalidationfeatures.
TableViewEditModel
Ifyouarepressedforscreenrealestateanddonothavespaceforamaster/detailsetup
witha TableView,aneffectiveoptionistoeditthe TableViewdirectly.Byenablingafew
streamlinedfeaturesinTornadoFX,youcannotonlyenableeasycelleditingbutalsoenable
dirtystatetracking,committing,androllback.Bycalling enableCellEditing()and
enableDirtyTracking(),aswellasaccessingthe tableViewEditModelpropertyofa
TableView,youcaneasilyenablethisfunctionality.
Whenyoueditacell,ablueflagwillindicateitsdirtystate.Calling rollback()willrevert
dirtycellstotheiroriginalvalues,whereas commit()willsetthecurrentvaluesasthenew
baseline(andremovealldirtystatehistory).
11.EditingModelsandValidation
177
importtornadofx.*
classMyApp:App(MyView::class)
classMyView:View("MyView"){
valcontroller:CustomerControllerbyinject()
vartableViewEditModel:TableViewEditModel<Customer>bysingleAssign()
overridevalroot=borderpane{
top=buttonbar{
button("COMMIT").setOnAction{
tableViewEditModel.commit()
}
button("ROLLBACK").setOnAction{
tableViewEditModel.rollback()
}
}
center=tableview<Customer>{
items=controller.customers
isEditable=true
column("ID",Customer::idProperty)
column("FIRSTNAME",Customer::firstNameProperty).makeEditable()
column("LASTNAME",Customer::lastNameProperty).makeEditable()
enableCellEditing()//enableseasiercellnavigation/editing
enableDirtyTracking()//flagscellsthataredirty
tableViewEditModel=editModel
}
}
}
classCustomerController:Controller(){
valcustomers=listOf(
Customer(1,"Marley","John"),
Customer(2,"Schmidt","Ally"),
Customer(3,"Johnson","Eric")
).observable()
}
classCustomer(id:Int,lastName:String,firstName:String){
vallastNameProperty=SimpleStringProperty(this,"lastName",lastName)
varlastNamebylastNameProperty
valfirstNameProperty=SimpleStringPorperty(this,"firstName",firstName)
varfirstNamebyfirstNameProperty
validProperty=SimpleIntegerProperty(this,"id",id)
varidbyidProperty
}
11.EditingModelsandValidation
178
Figure11.4A TableViewwithdirtystatetracking,with rollback()and commit()
functionality.
Notealsotherearemanyotherhelpfulpropertiesandfunctionsonthe TableViewEditModel.
The itemspropertyisan ObservableMap<S,TableColumnDirtyState<S>>mappingthedirty
stateofeachrecorditem S.Ifyouwanttofilteroutandcommitonlydirtyrecordssoyou
canpersistthemsomewhere,youcanhaveyour"Commit" Buttonperformthisaction
instead.
button("COMMIT").action{
tableViewEditModel.items.asSequence()
.filter{it.value.isDirty}
.forEach{
println("Committing${it.key}")
it.value.commit()
}
}
Therearealso commitSelected()and rollbackSelected()toonlycommitorrollbackthe
selectedrecordsinthe TableView.
11.EditingModelsandValidation
179
OSGi
ThischapterisgearedprimarilytowardsfolkswhoalreadyhavefamiliaritywithOSGi,which
standsforOpenServicesGatewayInitiative.TheideabehindOSGiisaddingand
removingmodulestoaJavaapplicationwithouttheneedforrestarting.TornadoFXsupports
OSGiandallowshighlymodularanddynamicapplications.
IfyouhavenointerestinOSGicurrently,youarewelcometoskipthischapter.However,itis
highlyrecommendedtoatleastknowwhatitissoyoucanidentifymomentsinthefuture
thatmakeithandy.
TornadoFXcomeswiththemetadataneededforanOSGiruntimetodetectandenableit.
Whenthe tornadofx.jarisloadedinanOSGicontainer,anumberofservicesare
automaticallyinstalledintheruntime.Theseservicesenablesomeveryinterestingfeatures
whichwewilldiscuss.
OSGiIntroduction
PleasebefamiliarwiththebasicsofOSGibeforeyoucontinuethischapter.Togetaquick
overviewofOSGitechnologyyoucancheckoutthetutorialsontheOSGiAlliancewebsite.
TheApacheFelixtutorialsarealsoagoodstartingpointreferenceforbasicOSGipatterns.
Services
WhentheTornadoFXJARisloaded,youcancreateyourownTornadoFXbundleandcreate
yourapplicationanywayyoulike.However,someusagepatternsaresotypicalanduseful
thatTornadoFXhasbuilt-insupportforthem.
DynamicApplications
ThedynamicnatureofOSGilendsitselfwelltoGUIapplicationsingeneral.Theabilityto
havecertainfunctionalitycomeandgoastheenvironmentchangescanbepowerful.
JavaFXitselfisunfortunatelywritteninawaythatpreventsyoufromstartinganother
JavaFXapplicationaftertheinitialapplicationshutsdown.Tocircumventthisshortcoming
andenableyoutostopandstartyourapplicationasmanytimesasyouwant,TornadoFX
providesawaytoregisteryour Appclasswithanapplicationproxywhichwillkeepthe
JavaFXenvironmentrunningevenwhenyourapplicationshutsdown.
12.OSGi
180
Togetstarted,implementa BundleActivatorthatprovidesameansto start()and
stop()an App.Registeringyourapplicationforthisfunctionalitycanbedonebycalling
context.registerApplicationwithyour Appclassasthesingleparameterinyourbundle
Activator:
classActivator:BundleActivator{
overridefunstart(context:BundleContext){
context.registerApplication(MyApp::class)
}
overridefunstop(context:BundleContext){
}
}
IfyoupreferOSGideclarativeservicesinstead,thiswillhavethesameeffectprovidedthat
youhavetheOSGiDSbundleloaded:
@Component
classAppRegistration:ApplicationProvider{
overridevalapplication=MyApp::class
}
ProvidedthattheTornadoFXbundleisavailableinyourcontainer,thisisenoughtostart
yourapplicationautomaticallyoncethebundleisactivated.Youcannowstopandstartitas
manytimesasyoulikebystoppingandstartingthebundle.
DynamicStylesheets
Youcanprovidetype-safestylesheetstootherTornadoFXbundlesbyregisteringtheminan
Activator:
classActivator:BundleActivator{
overridefunstart(context:BundleContext){
context.registerStylesheet(Styles::class)
}
overridefunstop(context:BundleContext){
}
}
UsingOSGiDeclarativeServicestheregistrationlookslikethis:
12.OSGi
181
@Component
classStyleRegistration:StylesheetProvider{
overridevalstylesheet=Styles::class
}
Wheneverthisbundleisloaded,everyactive Viewwillhavethisstylesheetapplied.When
thebundleisunloaded,thestylesheetisautomaticallyremoved.Ifyouwanttoprovide
multiplestylesheetsbasedonthesamestyleclasses,itisagoodideatocreateonebundle
thatexportsthe cssclassdefinitions,sothatyourViewscanreferencethesestyles,and
thestylesheetbundlescancreateselectorsbasedonthem.
DynamicViews
AcoolaspectofOSGiistheabilitytohaveUIelementspopupwhentheybecomeavailable.
Atypicalusecasecouldbea"dashboard"application.Inthisexample,thebaseapplication
bundlecontainsa ViewthatcanholdotherViews,andtellstheTornadoFXOSGiRuntime
thatitwouldliketoautomaticallyembedViewsiftheymeetcertaincriteria.
Forinstance,wecancreatea Viewthatcontainsa VBox.WetelltheTornadoFXOSGi
RuntimethatwewouldliketohaveotherViewsembeddedintoitiftheyaretaggedwiththe
discriminatordashboard:
classDashboard:View(){
overridevalroot=VBox()
init{
title="DashboardApplication"
addViewsWhen{it.discriminator=="dashboard"}
}
}
Ifthe addViewsWhenfunctionreturnstrue,the Viewisaddedtothe VBox.ToofferupViews
tothisDashboard,anotherbundlewoulddeclarethatitwantstoexportit'sViewbysetting
the dashboarddiscriminator.Hereweregisterafictive MusicPlayerviewtobedockedinto
thedashboardwhenit'sbundlebecomesactive.
classActivator:BundleActivator{
overridefunstart(context:BundleContext){
context.registerView(MusicPlayer::class,"dashboard")
}
overridefunstop(context:BundleContext){
}
}
12.OSGi
182
Again,theOSGiDeclarativeServiceswayofexportingtheViewwouldlooklikethis:
@Component
classMusicPlayerRegistration:ViewProvider{
overridevaldiscriminator="dashboard"
overridefungetView()=find(MusicPlayer::class)
}
The addViewsWhenfunctionissmartenoughtoinspectthe VBoxandfindouthowtoadd
thechildViewitwaspresented.Itcanalsofigureoutthatifyoucallthefunctionona
TabPaneitwouldcreateanew TabandsetthetitletothechildViewtitleetc.Ifyouwould
liketodosomethingcustomwiththepresentedViews,youcanreturn falsefromthe
functionsothatthechildViewwillnotbeaddedautomaticallyandthendowhateveryou
wantwithit.EventhoughtheTabexampleissupportedoutofthebox,youcoulddoit
explicitlylikethis:
tabPane.addViewsWhen{
if(it.discriminator=="dashboard"){
valview=it.getView()
tabPane.tab(view.title,view.root)
}
false
}
ManualhandlingofdynamicViews
CreateyourfirstOSGibundle
Agoodstartingpointisthe tornadofx-maven-osgi-projecttemplateintheTornadoFXIntelliJ
IDEAplugin.ThiscontainseverythingyouneedtobuildOSGibundlesfromyoursources.
TheOSGIIDEApluginmakesitveryeasytosetupandrunanOSGicontainerdirectlyfrom
theIDE.Thereisascreencastathttps://www.youtube.com/watch?v=liOFCH5MMKkthat
showstheseconceptsinaction.
OSGiConsole
TornadoFXhasabuiltinOSGiconsolefromwhichyoucaninspectbundles,changetheir
stateandeveninstallnewbundleswithdraganddrop.Youcanbringuptheconsolewith
Alt-Meta-Oorconfigureanothershortcutbysetting FX.osgiConsoleShortcutor
programmaticallyopeningthe OSGIConsoleView.
12.OSGi
183
Requirements
TorunTornadoFXinanOSGicontainer,youneedtoloadtherequiredbundles.Usuallythis
isamatterofdumpingthesejarsintothe bundledirectoryofthecontainer.Notethatany
jarthatistobeusedinanOSGicontainerneedstobe"OSGienabled",whicheffectively
meansaddingsomeOSGispecificentriesthe META-INF/MANIFEST.MFfile.
WeprovidedacompleteinstallationwithApacheFelixandTornadoFXalreadyinstalledat
http://tornadofx.tornado.no/felix-tornadofx-5.4.0.zip.Remembertoswapthe tornadofx.jar
forthelatestversion,asthisbundleismostlikelylaggingacoupleofversionsbehind.
ThesearetherequiredartifactsforanyTornadoFXapplicationrunninginanOSGicontainer.
Yourcontainermightalreadybebundlewithsomeofthese,socheckthecontainer
documentationforfurtherdetails.
12.OSGi
184
Artifact Version Binary
JavaFX8OSGiSupport 8.0 jar
TornadoFX 1.5.5 jar
KotlinOSGIBundle* 1.0.3 jar
ConfigurationAdmin** 1.8.10 jar
CommonsLogging 1.2 jar
ApacheHTTP-Client 4.5.2 jar
ApacheHTTP-Core 4.4.5 jar
JSON 1.0.4 jar
*TheKotlinOSGibundlecontainsspecialversionsof kotlin-stdliband kotlin-
reflectwiththerequiredOSGimanifestinformation.
**ThislinkstotheApacheFeliximplementationoftheOSGiConfigAdmininterface.Feel
freetousetheimplementationfromyourOSGicontainerinstead.Somecontainers,like
ApacheKaraf,alreadyhastheConfigAdminbundleloaded,soyouwon'tneeditthere.
12.OSGi
185
13.TornadoFXIDEAPlugin
TosavetimeinusingTornadoFX,youcaninstallaconvenientIntellijIDEApluginto
automaticallygenerateprojecttemplates,Views,injections,andotherTornadoFXfeatures.
Ofcourse,youdonothavetousethispluginwhichwasdonethroughoutthisbook.Butit
addssomeconveniencetobuildTornadoFXapplicationsalittlemorequickly.
InstallingthePlugin
IntheIntellijIDEAworkspace,pressCONTROL+SHIFT+Aandtype"Plugins",thenpress
ENTER.Youwillseeadialogtosearchandinstallplugins.ClicktheBrowseRepositories
button(Figure13.1).
Figure13.1AfterbringingupthePluginsdialog,clickBrowseRepositories.
13.TornadoFXIDEAPlugin
186
Youwillthenseealistof3rdpartypluginsavailabletoinstall.Searchfor"TornadoFX",select
it,andclickthegreenInstallbutton(Figure13.2).
Figure13.2Searchfor"TornadoFX"andclickInstall
WaitforittofinishinstallingandtherestartIntellijIDEA.
TornadoFXProjectTemplates
TheTornadoFXpluginshassomeMavenandGradleprojecttemplatestoquicklycreatea
configuredTornadoFXapplication.
InIntellijIDEA,navigatetoFile->New->Project...(Figure13.3).
Figure13.3
13.TornadoFXIDEAPlugin
187
YouwillthenseeadialogtocreateanewTornadoFXproject.YoucancreateGradleand
Mavenflavors,withorwithoutOSGisupport.Let'screateaGradleonefordemonstration
(Figure13.4).
Figure13.4
13.TornadoFXIDEAPlugin
188
Inthenextdialog,giveyourprojectaname,alocationfolder,andabasepackagewithyour
domain(Figure13.5).ThenclickFinish.
Figure13.5
YoumaybepromptedtoimporttheprojectasaGradleproject,andclickonthatpromptif
youencounterit.YouwillthenhaveaTornadoFXapplicationconfiguredandsetup,
including App, View,and Stylesentitiessetup(Figure13.6).
Figure13.6
13.TornadoFXIDEAPlugin
189
AgeneratedTornadoFXprojectwithaGradleconfiguration.
ThesestepsapplytotheMavenandOSGiwizardsaswell,anddonotforgettoputyour
projectonaversiontrackingsystemlikeGIT!.
CreatingViews
YoucancreateViews,Fragments,andFXMLfilesquicklywiththeplugin.Youcanrightclick
afolderintheProject,thennavigatethepopupmenutoNew->TornadoFXView(Figure
13.7).
Figure13.7
13.TornadoFXIDEAPlugin
190
Youwillthencometoadialogtodictatehowthe Viewisconstructed.Youevenhavethe
optionofspecifyingitasa FragmentinsteadthroughtheTypeparameter,aswellasan
FXMLviaKind.Finally,youcanspecifythe NodetypefortheRoot,whichshoulddefaultto
a BorderPane.
Figure13.8
ClickOKandanew Viewwillgeneratedandaddedtoyourproject(Figure13.9).
Figure13.9Anew ViewgeneratedwiththeTornadoFXplugin
13.TornadoFXIDEAPlugin
191
InjectingComponents
Onelastminorconvenience.YoucangenerateTornadoFX Componentinjectionsquickly
withtheplugin.Forinstance,ifyourightclicktheclassbodyofthe MainView,youcan
generatethe MyOtherViewasaninjectedproperty(Figure13.10).
Figure13.10
13.TornadoFXIDEAPlugin
192
Youcanthenuseadialogtoselectthe MyOtherViewastheinjectedproperty,thenclickOK
(Figure13.11).
Figure13.11
13.TornadoFXIDEAPlugin
193
GeneratingTornadoFXProperties
OneofthemosthelpfulfeaturesinthepluginistheabilitytoconvertplainKotlinproperties
intoTornadoFXproperties.
Sayyouhaveasimpledomainclasscalled Client.
classClient(id:Int,name:String){
valid:Int=id
valname:String=name
}
Ifyouclickonapropertyandthentheintentlightbulb,orpressALT+ENTER,youshouldsee
amenupopupwithanoptiontoconvertittoaTornadoFXProperty(Figure13.12).
Figure13.12
13.TornadoFXIDEAPlugin
194
Dothisforeachpropertyandyour Clientclassshouldnowlooklikethis.
classClient(id:Int,name:String){
varidbyproperty(id)
funidProperty()=getProperty(Client::id)
varnamebyproperty(name)
funnameProperty()=getProperty(Client::name)
}
Your ClientnowusesJavaFXpropertiesinsteadofplainproperties.Noticetheprimary
constructorwillpasstheintialvaluestothe property()delegates,butyoudonothaveto
provideinitialvaluesiftheyarenotdesired.
Thisisatime-savingfeaturewhencreatingdomaintypesfordatacontrols.Nextwewill
coverhowtogenerate TableViewcolumns.
GeneratingColumnsforaTableView
Anotherhandyfeatureyoucandowiththepluginalsoisgeneratingcolumnsfora
TableView.Ifyouhavea TableView<Person>,youcanputthecursoronitsdeclaration,
pressALT+ENTER,andgetaprompttogeneratethecolumns(Figure13.13).
Figure13.13
13.TornadoFXIDEAPlugin
195
Youwillthenseeadialogtoconfirmwhich Personpropertiestogeneratethecolumnson
(Figure14.14).
Figure13.14
Press"OK"andthecolumnswillthenbegeneratedforyou(Figure13.15).
Figure13.15
13.TornadoFXIDEAPlugin
196
Notethatatthetimeofwritingthisguide,foragiven TableView<T>,thisfeatureonlyworksif
thepropertieson TfollowtheJavaFXconventionusingthe Propertydelgates.
Summary
TheTornadoFXpluginhassometime-savingconveniencesthatyouarewelcometotake
advantageof.Ofcourse,youdonothavetousethepluginbecauseitmerelyprovides
shortcutsandgeneratescode.Intime,theremaybemorefeaturesaddedtothepluginso
besuretofollowtheprojectonGitHubforfuturedevelopments.
13.TornadoFXIDEAPlugin
197
14.Scopes
Scopeisasimpleconstructthatenablessomeinterestingandhelpfulbehaviorina
TornadoFXapplication.
Whenyouuse inject()or find()tolocatea Controllerora View,youwillbydefault
getbackasingletoninstance,meaningthatwhereveryoulocatethatobjectinyourcode,
youwillgetbackthesameinstance.Scopesprovideawaytomakea Viewor Controller
uniquetoasmallersubsetofinstancesinyourapplication.
ItcanalsobeusedtorunmultipleversionsofthesameapplicationinsidethesameJVM,for
examplewithJPro,whichexposesTornadoFXapplicationinawebbrowser.
AMaster/Detailexample
InanMDIApplicationyoucanopenaneditorinanewwindow,andensurethatallthe
injectedresourcesareuniquetothatwindow.Wewillleveragethattechniquetocreatea
personeditorthatallowsyoutoopenanewwindowtoediteachperson.
Westartbydefiningatableinterfacewhereyoucandoubleclicktoopenthepersoneditorin
aseparatewindow.
classPersonList:View("PersonList"){
valctrl:PersonControllerbyinject()
overridevalroot=tableview<Person>(){
column("#",Person::idProperty)
column("Name",Person::nameProperty)
onUserSelect{editPerson(it)}
asyncItems{ctrl.people()}
}
funeditPerson(person:Person){
valeditScope=Scope()
valmodel=PersonModel()
model.item=person
setInScope(model,editScope)
find(PersonEditor::class,editScope).openWindow()
}
}
14.Scopes
198
The editfunctioncreatesanew Scopeandinjectsa PersonModelconfiguredwiththe
selecteduserintothatscope.Finally,itretrievesa PersonEditorinthecontextofthenew
scopeandopensanewwindow.
Whenthe PersonEditorisinitialized,itwilllookupa PersonModelviainjection.Thedefault
contextfor injectand findisalwaysthescopethatcreatedthecomponent,soitwill
lookinthe personScopewejustcreated.
valmodel:PersonModelbyinject()
BreakingOutoftheCurrentScope
Whennoscopeisdefined,injectableresourcesarelookedupinthedefaultscope.Thereis
anitemrepresentingthatscopecalled DefaultScope.Intheaboveexample,theeditor
mighthavecalledouttoa PersonControllertoperformasaveoperationinadatabaseor
viaaRESTcall.This PersonControllerismostprobablystateless,sothereisnoneedto
createaseparatecontrollerforeacheditwindow.Toaccessthesamecontrollerinalleditor
windows,wesupplythescopewewanttofindthecontrollerin:
valcontroller:PersonControllerbyinject(DefaultScope)
Thiseffectivelymakesthe PersonControlleratruesingletonobjectagain,withonlyasingle
instanceinthewholeapplication.
Thedefaultscopefornewinjectedobjectsarealwaysthecurrentscopeforthecomponent
thatcalls injector find,andconsequentlyallobjectscreatedinthatinjectionrunwill
belongtothesuppliedscope.
KeepingStateinScopes
Inthepreviousexampleweusedinjectiononascopeleveltogetaholdofourresources.It
isalsopossibletosubclass Scopeandputarbitrarydatainthere.EachTornadoFX
Componenthasa scopepropertythatgivesyouaccesstothatscopeinstance.Youcan
evenoverrideittoprovidethecustomsubclasssoyoudon'tneedtocastitonevery
occasion:
overridevalscope=super.scopeasPersonScope
14.Scopes
199
Nowwheneveryouaccessthe scopepropertyfromyourcode,itwillbeoftype
PersonScope.Itnowcontainsa PersonModelthatwillonlybeavailabletothisscope:
classPersonScope:Scope(){
valmodel=PersonModel()
}
Let'schangeourpreviousexampleslightlytoaccessthemodelinsidethescopeinsteadof
usinginjection.FirstwechangetheeditPersonfunction:
funeditPerson(person:Person){
valeditScope=PersonScope()
editScope.model.item=person
find(PersonEditor::class,editScope).openWindow()
}
Thecustomscopealreadyhasaninstanceof PersonModel,sowejustconfiguretheitemfor
thatscopeandopentheeditor.Nowtheeditorcanoverridethetypeofscopeandaccess
themodel:
//Castscope
overridevalscope=super.scopeasPersonScope
//Extractourviewmodelfromthescope
valmodel=scope.model
Bothapproachesworkequallywell,butdependingonyourusecaseyoumightpreferone
overtheother.
Globalapplicationscope
Aswehintettoinitially,youcanrunmultipleapplicationsinthesameJVMandkeepthem
completelyseparatebyusingscopes.Bydefault,JavaFXdoesnotsupportmultitenancy,
andcanonlystartasingleJavaFXapplicationperJVM,butnewtechnologiesareemerging
thatleveragesmultitenancyandwillevenexposeyourJavaFXbasedapplicationstothe
web.OnesuchtechnologyisJPro.io,andTornadoFXsupportsmultitenancyforJPro
applicationsbyleveragingscopes.
ThereisnospecialJProclassesinTornadoFX,butsupportingJProisverysimpleby
leverangingscopes:
UsingTornadoFXwithJPro
14.Scopes
200
JProwillcreateanewinstanceofyourAppclassforeachnewwebuser.Also,toaccessthe
JProWebAPIyouneedtogetaccesstothestagecreatedforeachuser.Inthisexamplewe
subclass ScopetocreateaspecialJProScopethatcontainsthestagethatwasgivento
eachapplicationinstance:
classJProScope(valstage:Stage):Scope(){
valwebAPI:WebAPIget()=WebAPI.getWebAPI(stage)
}
Thenextstepistosubclass JProApplicationtodefineourentrypoint.Thisappclassisin
additiontoourexistingTornadoFXAppclass,whichbootstheactualapplication:
classMain:JProApplication(){
valapp=OurTornadoFXApp()
overridefunstart(primaryStage:Stage){
app.scope=JProScope(primaryStage)
app.start(primaryStage)
}
overridefunstop(){
app.stop()
super.stop()
}
}
Wheneveranewuservisitsoursite,the Mainclassiscreated,togetherwithanew
instanceofouractualTornadoFXapplication.
Inthe startfunctionweassignanew JProScopetotheTornadoFXappinstanceandthen
call app.start.Fromthereonout,allinstancescreatedusing injectand findwillbein
thecontextofthatJProinstance.
Asusual,youcanbreakoutofthe JProScopetoaccessJVMlevelglobalsbysupplyingthe
DefaultScopeoranyothersharedscopetothe injector findfunctions.
WeshouldprovideautilityfunctionthatmakesiteasytoaccesstheJProWebAPIfromany
Component:
valComponent.webAPI:WebAPIget()=(scopeasJProScope).webAPI
The scopepropertyofany Componentwillbethe JProScopesowecancastitandaccess
the webAPIpropertywedefinedinourcustomscopeclass.
14.Scopes
201
14.Scopes
202
15.EventBus
An EventBusisaversatiletoolwithamultitudeofusecases.Dependingonyourcoding
styleand
preferences,youmightwanttoreducecouplingbetweencontrollersandviewsbypassing
messages
insteadofhavinghardreferencestoeachother.TheTornadoFXeventbuscanmakesure
thatthemessagesarereceivedontheappropriatethread,withouthavingtodothat
concurrencyhouse-keepingmanually.
Peopleuseeventbusesformanydifferentusecases.TornadoFXdoesnotdictatewhenor
howyoushould
usethem,butwewanttoshowyousomeoftheadvantagesitcanprovidetoyou.
StructureoftheEventBus
Aswithanytypicaleventbusimplementation,youcanfireeventsaswellassubscribeand
unsubscribe
toeventsonthebus.Youcreateaneventbyextendingthe FXEventclass.Insomecases,
anevent
canbejustasignaltosomeothercomponenttotriggersomethingtohappen.Inother
cases,the
eventcancontaindatawhichwillbebroadcasttothesubscribersofthisevent.Letuslookat
acoupleof
eventdefinitionsandhowtousethem.
PictureaUIwheretheusercanclicka Buttontorefreshalistofcustomers.The View
knowsnothingofwherethedataiscomingfromorhowitisproduced,butitsubscribes
tothedataeventsandusesthedataonceitarrives.Letuscreatetwoeventclassesforthis
usecase.
Firstwedefineaneventsignaltypetonotifyanylistenersthatwewantsomecustomerdata:
importtornadofx.EventBus.RunOn.*
objectCustomerListRequest:FXEvent(BackgroundThread)
15.EventBus
203
Thiseventobjectisanapplication-wide object.Becauseitwillneverneedtocontaindata,
itwillsimplybe
broadcasttosaythatwewantthecustomerlist.The RunOnpropertyissetto
BackgroundThread,tosignal
thatthereceiverofthiseventshouldoperateoffoftheJavaFXApplicationThread.That
meansitwillbe
givenabackgroundthreadbydefault,sothatitcandoheavyworkwithoutblockingtheUI.
Intheexampleabove,wehaveaddedastaticimportforthe RunOnenum,sothatwe
justwrite BackgroundThreadinsteadof EventBus.RunOn.BackgroundThread.YourIDEwillhelp
youtomakethisimportsoyour
codelookscleaner.
AbuttonintheUIcanfirethiseventbyusingthe firefunction:
button("Loadcustomers").action{
fire(CustomerListRequest)
}
A CustomerControllermightlistenforthisevent,andloadthecustomerlistondemand
beforeitfiresanevent
withtheactualcustomerdata.Firstweneedtodefineaneventthatcancontainthe
customerlist:
classCustomerListEvent(valcustomers:List<Customer>):FXEvent()
Thiseventisa classratherthanan object,asitwillcontainactualdataandvary.Also,it
didnotspecifyanothervalueforthe RunOnproperty,sothiseventwillbeemittedonthe
JavaFXApplicationThread.
Acontrollercannowsubscribetoourrequestfordataandemitthatdataonceithasit:
classCustomerController:Controller(){
init{
subscribe<CustomerListRequest>{
valcustomers=loadCustomers()
fire(CustomerListEvent(customers))
}
}
funloadCustomers():List<Customer>=db.selectAllCustomers()
}
BackinourUI,wecanlistentothiseventinsidethecustomertabledefinition:
15.EventBus
204
tableview<Customer>{
column("Name",Customer::nameProperty)
column("Age",Customer::ageProperty)
subscribe<CustomerListEvent>{event->
items.setAll(event.customers)
}
}
Wetelltheeventbusthatweareinterestedin CustomerListEvents,andoncewehavesuch
aneventwe
extractthecustomersfromtheeventandsetthemintothe itemspropertyofthe
TableView.
QueryParametersInEvents
Aboveyousawasignalusedtoaskfordata,andaneventreturnedwiththatdata.The
signalcouldjustaswell
containqueryparameters.Forexample,itcouldbeusedtoaskforaspecificcustomer.
Imaginetheseevents:
classCustomerQuery(valid:Int):FXEvent(false)
classCustomerEvent(valcustomer:Customer):FXEvent()
Usingthesameprocedureasabove,wecannowsignalourneedforaspecific Customer,
butwenowneedtobe
morecarefulwiththedatawegetback.IfourUIallowsformultiplecustomerstobeeditedat
once,weneedto
makesurethatweonlyapplydataforthecustomerweaskedfor.Thisisquiteeasily
accountedforthough:
15.EventBus
205
classCustomerEditor(valcustomerId:Int):View(){
valmodel:CustomerModel
overridevalroot=form{
fieldset("Customerdata"){
field("Name"){
textfield(model.name)
}
//Morefieldsandbuttonshere
}
}
init{
subscribe<CustomerEvent>{
if(it.customer.id==customerId)
model.item=it.customer
}
fire(CustomerQuery(customerId))
}
}
TheUIiscreatedbeforetheinterestingbithappensinthe initfunction.First,we
subscribeto CustomerEvents,
butwemakesuretoonlyactonceweretrievethecustomerwewereaskingfor.Ifthe
customerIdmatches,
weassignthecustomertothe itempropertyofour ItemViewModel,andtheUIisupdated.
Anicesideeffectofthisisthatourcustomerobjectwillbeupdatedwheneverthesystem
emitsnewdata
forthiscustomer,nomatterwhoaskedforthem.
EventsandThreading
Whenyoucreateasubclassof FXEvent,youdictatethevalueofthe runOnproperty.Itis
ApplicationThread
bydefault,meaningthatthesubscriberwillreceivetheeventontheJavaFXApplication
Thread.Thisisusefulforevents
comingfromandgoingtootherUIcomponents,aswellasbackendservicessendingdatato
theUI.Ifyouwantto
signalsomethingtoabackendservice,onewhichislikelytoperformheavy,long-running
work,youshouldset runOn
to BackgroundThread,makingsurethesubscriberwilloperateoffoftheUIthread.The
subscribernownolongerneedsto
makesurethatitisoffoftheUIthread,soyouremovealotofthread-relatedhousekeeping
15.EventBus
206
calls.Usedcorrectly
thisisconvenientandpowerful.Usedincorrectly,youwillhaveanonresponsiveUI.Make
sureyouunderstand
thiscompletelybeforeplayingwithevents,oralwayswraplongrunningtasksin runAsync
{}.
Scopes
Theeventbusemitsmessagesacrossallscopesbydefault.Ifyouwanttolimitsignalstoa
certainscope,you
cansupplythesecondparametertothe FXEventconstructor.Thiswillmakesurethatonly
subscribersfromthe
givenscopewillreceiveyourevent.
classCustomerListRequest(scope:Scope):FXEvent(BackgroundThread,scope)
The CustomerListRequestisnotanobjectanymoresinceitneedstocontainthescope
parameter.Youwouldnowfire
thiseventfromanyUIComponentlikethis:
button("Loadcustomers").action{
fire(CustomerListRequest(scope))
}
Thescopeparameterfromyour UIComponentispassedintothe CustomerListRequest.
Whencustomerdatacomes
back,theframeworktakescareofdiscriminatingonscopeandonlyapplytheresultsifthey
aremeantforyou.You
donotneedtomentionthescopetothesubscribefunctioncall,astheframeworkwill
associateyoursubscription
withthescopeyourareinatthetimeyoucreatethesubscription.
subscribe<CustomerListEvent>{event->
items.setAll(event.customers)
}
InvalidationofEventSubscribers
15.EventBus
207
Inmanyeventbusimplementations,youareleftwiththetaskofderegisteringthe
subscriberswhenyourUIcomponents
shouldnolongerreceivethem.TornadoFXtakesanopinionatedapproachtoeventcleanup
soyoudonothavetothinkaboutitmuch.
Subscriptionsinside UIComponentslike Viewand Fragmentareonlyactivewhenthat
componentisdocked.Thatmeansthatevenifyouhavea Viewthathasbeenpreviously
initializedandused,eventsubscriptionswillnotreachitunlessthe Viewisdockedinsidea
windoworsomeothercomponent.Oncetheviewisdocked,theeventswillreachit.Onceit
isundocked,theeventswillnolongerbedeliveredtoyourcomponent.Thistakescareof
theneedforyoutomanuallyderegistersubscriberswhenyoudiscardofaview.
For Controllershowever,subscriptionsarealwaysactiveuntilyoucall unsubscribe.You
needtokeep
inmindthatcontrollersarelazilyloaded,soifnothingreferencesyourcontroller,the
subscriptionswill
neverberegisteredinthefirstplace.Ifyouhavesuchacontrollerwithnootherreferences,
butyouwant
ittosubscribetoeventsrightaway,agoodplacetoeagerlyloaditwouldbethe initblock
ofyour Appsubclass:
classMyApp:App(MainView::class){
init{
//EagerlyloadCustomerControllersoitcanreceiveevents
find(CustomerController::class)
}
}
DuplicateSubscriptions
Toavoidregisteringyoursubscriptionsmultipletimes,makesureyoudonotregisterthe
eventsubscriptionsin onDock()oranyothercallbackfunctionthatmightbeinvokedmore
thanonceforthedurationofthecomponentlifecycle.Thesafestplacetocreateevent
subscriptionsisinthe initblockofthecomponent.
ShouldIuseeventsforUIlogiceverywhere?
Usingeventsforeverythingmightseemlikeanobleidea,andsomepeoplemightpreferit
becauseoftheloosecoupling
itfacilitates.However,the ItemViewModelwithinjectionisoftenamorestreamlinedsolution
15.EventBus
208
topassingdataandkeepingUIstate.Thisexamplewasprovidedtoexplainhowtheevent
systemworks,nottoconvinceyoutowriteyourUIsthiswayallthetime.
Manyfeelthateventsmightbebettersuitedforpassingsignalsratherthanactualdata,so
youmightalsoconsidersubscribingtosignalsandthenactivelyretrievingthedatayouneed
instead.
Unsubscribeaftereventisprocessed
Insomesituationsyoumightwanttoonlywanttotriggeryourlisteneracertainamountof
times.Admittedly,thisisnotveryconvenient.Youcanpassthe times=nparameterto
subscribetocontrolhowmanytimestheeventistriggeredbeforeitisunsubscribed:
objectMyEvent:FXEvent()
classMyView:View(){
overridevalroot=stackpane{
paddingAll=100
button("Fire!").action{
fire(MyEvent)
}
}
init{
subscribe<MyEvent>(times=2){
alert(INFORMATION,"Eventreceived!","Thismessageshouldonlyappeartwi
ce.")
}
}
}
Youcanalsomanuallyunsubscribebasedonanarbitrarycondition,orsimplyafterthefirst
run:
15.EventBus
209
classMyView:View(){
overridevalroot=stackpane{
paddingAll=100
button("Fire!").action{
fire(MyEvent)
}
}
init{
subscribe<MyEvent>{
alert(INFORMATION,"Eventreceived!","Thismessageshouldonlyappearonc
e.")
unsubscribe()
}
}
}
15.EventBus
210
Workspaces
JavaBusinessapplicationshavetraditionallybeenbasedononeoftheRichClient
Frameworks,
namelyNetBeansPlatformorEclipseRCP.AnimportantreasonforchoosinganRCP
platformhasbeenthe
workspacelikefunctionalitytheyprovide.Someimportantfeaturesofaworkspaceare:
Commonactionbuttonsthattietothestateofthedockedview(Save,Refreshetc)
ContextbasedUInodesaddedtothecommonworkspaceinterface
Navigationstackfortraversingvisitedviews,controlledthroughbackandforward
buttonslikeawebbrowser
Menusystemwithdynamiccontributionsandmodifications
TornadoFXhasbeguntobridgethegapbetweentheRCPplatformsbyproviding
Workspaces.Whilestillinit'sinfancy,
thedefaultfunctionalityisasolidfoundationforbusinessapplicationsinneedofthefeatures
discussedabove.
ThesimplestpossibleWorkspaceapp
TokickoffaWorkspaceapp,allyouneedtodoistosubclass Appandsettheprimary
Viewto Workspace::class.
Theresultcanbeseenbelow(Figure16.1).
classMyApp:App(Workspace::class)
Figure16.1
16.Workspaces
211
TheresultingWorkspaceconsistsofabuttonbarwithfourdefaultbuttonsandanempty
contentareabelowit.
Thecontentareacanhouseany UIComponent.Youaddacomponenttothe contentarea
bycalling workspace.dock()onit.Ifyou
showtheWorkspacewithoutadockedView,itwillbydefaultonlytakeupthespaceneeded
forthebuttons.ThewindowinFigure16.1
wasresizedafteritwasopened.
Let'spretendwehavea CustomerListcomponentthatwewouldliketodockinthe
Workspaceastheapplicationstarts.
Wedothisbyoverridingthe onBeforeShowcallback:
classMyApp:App(Workspace::class){
overridefunonBeforeShow(view:UIComponent){
workspace.dock<CustomerList>()
}
}
Figure16.2
16.Workspaces
212
ThecompletecodeoftheCustomerListisnotimportantforus,sufficeittosaythatit
displaysaTableViewand
listssomeCustomers.Whatisinterestinghowever,isthattheRefreshbuttoninthe
Workspacewasenabled
whenthe CustomerListwasdocked,whiletheSavebuttonremaineddisabled.
LeveragingtheWorkspacebuttons
Whenevera UIComponentisdockedintheWorkspace,theRefresh,SaveandDelete
buttonswillbeenabledbydefault.ThishappensbecausetheWorkspacelooksatthe
refreshable, savableand deletablepropertiesinthedockedcomponent.Every
UIComponentreturnsabooleanpropertywiththedefaultvalueof true,whichthe
Workspacethenconnectstotheenabledstateofthesebuttons.Inthe CustomerList
example,wemadesuretheSavebuttonwasalwaysdisabledbyoverridingthisproperty:
overridevalsavable=SimpleBooleanProperty(false)
Wecanachievethesameresultbycalling disableSave()inthe initblock,samegoesfor
disableRefresh()and disableDelete().
Wedidn'ttouchtheotherbuttons,sotheyremain trueasperthedefault.Wheneverthe
Refreshbutton
iscalled,itwillfirethe onRefreshfunctioninthe View.Youcanoverridethistoprovide
16.Workspaces
213
yourrefreshaction:
overridevalonRefresh(){
customerTable.asyncItems{customerController.listCustomers()}
}
SamegoesfortheDeletebutton.WewillrevisittheSavebuttonandintroduceaneattrick
toonlyactivateitwhentherearedirtychangeslaterinthischapter.
TabbedViews
YoumayatonepointdockaViewcontainingaTabPaneinsideofaWorkspace,andthen
addtabswhichrepresentsfurtherUIComponents.Youcanquiteeasilyproxythesavable,
refreshableanddeletablestateandactionsfromtheWorkspaceontotheViewrepresented
bythecurrentlyactiveTab.ConsideraCustomerEditorwhichhastabsforeditingcustomer
data,andoneforeditingcontactsforthatcustomer.Whenevertheuserselectsoneofthe
tabs,thebuttonsintheWorkspaceshouldinteractwiththestateandactionsfromthe
selectedtabview.
classCustomerEditor:View("CustomerEditor"){
overridevalroot=tabpane{
tab(CustomerBasicDataEditor::class)
tab(ContactListEditor::class)
connectWorkspaceActions()
}
}
Thatsinglecallto connectWorkspaceActions()takescareofeverythingforus.Theactual
implementationofthetwosubviewsareomittedforbrevity,butyoucanimaginethatthey
sharea CustomerViewModelinjectedintothescopetheyshareforexample.
Theactualimplementationof connectWorkspaceActionsisquitesimple,andrevealswhat's
goingonunderthecover:
funTabPane.connectWorkspaceActions(){
savableWhen{savable}
whenSaved{onSave()}
deletableWhen{deletable}
whenDeleted{onDelete()}
refreshableWhen{refreshable}
whenRefreshed{onRefresh()}
}
16.Workspaces
214
Thisfunctionisdeclaredinside UIComponent,sothe savableWhen, deletableWhenand
refreshableWhenareperformedontheUIComponent.Thosestatearethenboundtothe
savable, deletableand refreshablestateoftheTabPane.Butwait-aTabPanedoesn't
havethosefunctions?!Yes,inTornadoFXithas:)Youcanprobablyguessthatthe
implementationisagainanotherproxyintothecurrentlyselectedTabintheTabPane,anda
lookuptheUIComponentrepresentedbythe contentpropertyofthatTab.Wheneverthe
Tabchanges(orwhenthecontentofthetabchanges),theunderlyingUIComponentis
lookedup,andthepertinentstatesareboundtotheWorkspace.
Itwouldalsobepossibletobindthesestatesandconnecttheactionsmoreexplicitly.You
willneverorseldomneedtodothat,butthefollowing
examplemighthelpyourunderstandingoftheproxymechanism.
classTooExplicitCustomerEditor:View(){
overridevalroot=tab{
...
}
overridevalsavable=root.savable
overridevalrefreshable=root.refreshable
overridevaldeletable=root.deletable
overridefunonSave(){
root.onSave()
}
overridefunonDelete(){
root.onDelete()
}
overridefunonRefresh(){
root.onRefresh()
}
}
Asmentioned,youneverneedtodothisandshouldalwaysusethe
connectWorkspaceActionscall,butyoumightwanttooverrideoneof onSave, onDeleteor
onRefreshtoperformsomeactioninthemaineditorbeforecallingthesameactioninside
theactivetabbycalling root.onXXX.Let'ssaythattherefreshcallinthemaineditorreloads
thecustomer,butyoualsowanttohavethecontactlistrefreshifthatviewiscurrentlyactive.
Thiscouldbedone
likethis:
16.Workspaces
215
classCustomerEditor:View(){
valcustomerController:CustomerControllerbyinject()
valcustomer:CustomerModelbyinject()
overridevalroot=tabpane{
tab(CustomerBasicDataEditor::class)
tab(ContactListEditor::class)
connectWorkspaceActions()
}
overridevalonRefresh(){
runAsync{
customerController.getCustomer(customer.id.value)
}ui{
customer.item=it
root.onRefresh()
}
}
}
Thislittletrickenablesyoutohandletheactualreloadofthecustomerinthemainview
insteadofreimplementingitineverytab.
Forwardingbuttonstateandactions
Aswehaveseen,thecurrentlydockedViewcontrolstheWorkspacebuttons.Sometimes
youdocknestedViewsinsidethemainView,andyouwouldlikethatnestedViewtocontrol
thebuttonsandactionsinstead.Thiscaneasilybedonewiththe forwardWorkspaceActions
function.Youcanchangetheforwardinghoweveryouseefit,forexampleonfocusoron
clickonsomecomponentinsidethenestedView.
classCustomerEditor:View(){
overridevalroot=hbox{
valbasicDataEditor=find<CustomerBasicDataEditor>()
add(basicDataEditor)
forwardWorkspaceActions(basicDataEditor)
add(ContactListEditor::class)
}
}
Modifyingthedefaultworkspace
Thedefaultworkspaceonlygivesyoubasicfunctionality.Asyourapplicationgrowsyouwill
wanttosuplementthe
toolbarwithmorebuttonsandcontrols,andmaybea MenuBaraboveit.Forsmall
16.Workspaces
216
modificationsyoucanaugment
itinthe onBeforeFunctionaswedidabove,butyouwillmostprobablywanttosubclassas
thecustomizations
becomemoreadvanced.ThefollowingcodeandimageistakenfromarealworldCRM
application:
classCRMWorkspace:Workspace(){
init{
add(MainMenu::class)
add(RestProgressBar::class)
add(SearchView::class)
}
}
The CRMWorkspaceloadsthreeotherviewsintoit.Oneprovidinga MenuBar,thenthedefault
RestProgressBaris
added,andlastlya SearchViewprovidingasearchinputfieldisadded.
TheWorkspacehasaprettygoodideaaboutwheretoplacewhateveryouaddtoit.For
example,buttonswillbydefault
beaddedafterthefourdefaultbuttons,whileothercomponentsareaddedtothefarrightof
theToolBar.TheMenuBar
isautomaticallyaddedabovetheToolBar,atthetopofthescreen.
Figure16.3showshowitlooksinproduction,withalittlebitofcustomstylinganda
CustomerEditordockedintoit.
ThisapplicationhappenstobeinNorwegian,andsomeoftheinformationintheCustomer
cardhasbeenremoved.
Figure16.3
16.Workspaces
217
YouwillnoticethattheSavebuttonisenabledinthisView.Thisisbecausethe savable
propertyisboundto
thedirtystatepropertyoftheviewmodel:
valmodel:CustomerModelbyinject()
overridevalsavable=model.dirty
Whenacustomerisloaded,theSavebuttonwillstaydisableduntilanedithasbeenmade.
Tosave,weoverridethe onSavefunction:
overridefunonSave(){
runAsync{
customerController.save(customer.item)
}ui{saved->
customer.update(saved)
}
}
Thisparticular customerController.savecallwillreturnthe Customerfromtheserveronceit
issaved.Iftheservermadeanychanges
toourcustomerobject,theywouldhavebeenreflectedinthesavedcustomerwegotback.
Forthatreason,wecall
customer.update(saved)whichisfunctionyougetforfreeifyouimplement JsonModel.This
16.Workspaces
218
makessurethatchanges
fromtheserverispushedbackintothemodel.Thisiscompletelyoptional,andyoumight
justwanttodo customerController.save(customer.item).
Titleandheading
Whenaviewisdocked,thetitleoftheWorkspacewillmatchthetitleofthatview.Thereis
alsoaheading
textintheworkspacethatbydefaultshowsthesametextasthetitle.Theheadingcanbe
overridenbyassigningto
the headingvariableorbindingtothe headingPropertyproperty.Ifyouwanttocompletely
removetheheading,augment
theworkspacewith workspace.headingContainer.removeFromParent()orjusthideit.Youcan
alsoputwhatever
nodesyouwantinsidetheheadingcontainer.YousawthistrickintheCRMscreenshot,
whereaGravatoriconwasplaced
totheleftofthecustomername.
DynamicelementsintheToolBar
SomeviewsmightneedmorebuttonsorfunctionalityaddedtotheToolBar,butonceyou
navigateawayfromtheviewit
wouldn'tmakesensetokeepthemaround.TheWorkspacewillactuallytrackwhatever
elementsyouaddtoitwhileaviewis
dockedandremovethosechangeswhentheviewisundocked.Theperfectplacetoadd
theseextrabuttonswouldbethe onDock
calloftheview.
Every UIComponenthasapropertycalled workspacewhichwillpointtothecurrent
WorkspaceforthecurrentScope.Let's
addan"AddCustomer"buttontotheWorkspacewheneverthe CustomerListisdocked:
overridefunonDock(){
with(workspace){
button("AddCustomer").action{
addCustomer()
}
}
}
16.Workspaces
219
TheWorkspacewillnowlooklikeinFigure16.4
Itlookslikeadefaultbutton.Youcanremovetheborderaroundthebuttonbyaddingthe
icon-onlycssclasstoit.Optionally
youcanconfigureaniconforthegraphicnodeifyoulike.Thebuiltiniconsaresvgshapes
addedinthebuiltin workspace.css
butfeelfreetoaddyouriconinanywayyouseefit.Let'saddaniconfromthe
FontAwesomeFXlibraryandmakeitlooklike
theotherbuttons:
button("AddCustomer"){
addClass("icon-only")
graphic=FontAwesomeIconView(PLUS_CIRCLE).apply{
style{
fill=c("#818181")
}
glyphSize=18
}
action{addCustomer()}
}
16.Workspaces
220
Inarealapplicationyouwoulduseacssclasssoyoudon'tneedtoconfigurethefillfor
everybuttonyouadd.TheresultcanbeseeninFigure16.5:
Figure16.5
Navigatingbetweendockedviews
OurCustomerListisconfiguredsothatwheneveryoudoubleclickacustomeryouwillbe
takentoaneditorforthatcustomer.
TheTableViewbindstheselectedusertoa CustomerModelviewmodelobject,andthe
actionisperformedlikethis:
tableview(customers){
column("FirstName",Customer::firstNameProperty)
column("LastName",Customer::lastNameProperty)
bindSelected(model)
onUserSelect{workspace.dock<CustomerEditor>()}
}
16.Workspaces
221
Theonlythingweneedtodoisactuallydockthe CustomerEditorwhentheuserselectsa
row.Sincethe CustomerEditor
willbelookedupinthesamescopewearecurrentlyin,itwillhaveaccesstotheselected
customeraswell:
classCustomerEditor:Fragment("CustomerEditor"){
valcustomer:CustomerModelbyinject()
overridevalsavable=customer.dirty
overridevalheadingProperty=customer.fullName
overridevalroot=form{
fieldset("CustomerDetails"){
field("FirstName"){
textfield(customer.firstName)
}
field("LastName"){
textfield(customer.lastName)
}
}
}
overridefunonSave(){
customer.commit()
}
overridefunonRefresh(){
customer.rollback()
}
}
Thecustomermodelisinjected,andwillcontaintheselectedcustomerfromthelist.The
savablepropertyisbound
tothe dirtypropertyofthemodelandthe headingPropertyisboundtoa StringBinding
called fullName,which
concatinatesthefirstandlastnamesandupdateswhenevertheyarechanged.Theform
fieldsbindtothenameproperties
andlastlythe onSaveand onRefreshfunctionsareimplementedtoreacttothe
correspondingWorkspacebuttons.
Figure16.6
16.Workspaces
222
Wecanseethatthe titleand headingareindeeddisplayingseparateinformation.Since
wehaven'tmadeanyedits
yet,theSavebuttonisdisabled,whiletheRefreshbuttonisavailable,andwouldrollback
anychangesmade
sincethelastcommit.
The backbuttonisenabledaswell,andclickingitwouldnavigatebacktotheCustomer
list.Thisisaverypowerful
featurewhichenablesbrowserlikenavigationinyourapplicationwithverylittleeffortonyour
part.TheWorkspace
keepsanavigationstackofconfigurabledepth.Bydefaultitwillcontain10previously
dockedviews.Youcanconfigurethe
maxViewStackDepthtochangethenumberofviewsheldinthenavigationstack.
Alternativetooverriding onSaveand
onRefresh
16.Workspaces
223
Sometimesyouwanttoaccessanobjectinoneoftheworkbenchbuttonactionsbutyou
wanttoavoidcreatingavariable
forthatobject.Insteadyoucanusethe whenSavedand whenRefreshedcallbacks,whichcan
beconfiguredfromanywhere.
Important:Theyarealternativesto onSaveand onRefreshsoyoushouldonlydooneor
theother.Let'ssaywewantto
refreshaTableViewwhentheRefreshbuttonisclicked.Wecanconfigurethisinsidethe
builderfortheTableView:
tableview{
whenRefreshed{
asyncItems{controller.loadItems()}
}
}
Thisisahandyalternativeinsomesituations,butmakesureyouonlychooseoneofthe
strategies.
Advancedscopenavigation
Whenyouleverageinjectedviewmodelstogetherwithanavigationstack,someinteresting
challengesappearthatneed
tobeaddressed.IfyouremovedtheBackbutton
(workspace.backButton.removeFromParent())orsetthe maxViewStackDepthto
0youcandisregardthisparticularchallenge,buttoleveragethispowerfulnavigation
paradigm,therearesomethings
youneedtothinkabout.
Considerourprevousexamplewithaninjected CustomerModelthatrepresentsthecurrently
selectedcustomerinthe CustomerList
whilealsobeingusedbythe CustomerEditortoeditthatsamecustomer.Thenlet'sassume
thatthereisawaytosearchfor
acustomerandeditit,perhapsusinga TextFieldintheToolBaroftheWorkspaceasa
searchentrypoint.Ifyousearchfor
anewcustomerandgoontoeditit,thennavigatebacktothepreviouscustomereditor,it
wouldsuddenlyoperateonthe
lastcustomeryousetinthe CustomerModel.Youcanprobablyimaginetheensuinghavoc.
Fortunately,thescopingsupportstretchesfarintotheWorkspacefeatureandprovidessome
handytoolsforthisparticularsituation.
16.Workspaces
224
WeneedtofindawaytocontainthescopeforthepairofCustomerListandCustomerEditor
sotheycanworktogetherwhileallowing
otherviewstousethe CustomerModel,butinadifferentscope.It'sactuallyquiteeasy.
Wheneveryoucreateanew CustomerList,
alsocreateanewScope.Ifyouweretodothismanually,itwouldlooksomethinglikethis:
//Createanewscope,butkeepthecurrentworkspace
valnewScope=Scope(workspace)
//FindtheCustomerListinthenewscope
valcustomerList=find<CustomerList>(newScope)
//DockthecustomerListintheworkspace
workspace.dock(customerList)
Thosethreedistinctoperationscanbeperformedinasinglecall:
workspace.dockInNewScope<CustomerList>()
WhentheCustomerListdockstheCustomerEditorlateron,ithappensinthisnewscope.
Butwhataboutthesearchfield?
Wewouldneedtoprovideaseparatescopeforthe CustomerEditorthatshouldshowthe
resultofthesearch,but
werewewouldalsoalsoneedtoinjectthecustomermodelcontainingtheselected
customerintothenewscope.This
followingcodeisimaginedinsidetheactionthatselectsacustomerfromthesearchresult:
funeditCustomer(customer:Customer){
//Createaviewmodelforthecustomer
valmodel=CustomerModel(customer)
//Createanewscope,butkeepthecurrentworkspace
valnewScope=Scope(workspace)
//Insertthecustomermodelintothenewscope
newScope.set(model)
//FindtheCustomerEditorinthenewscope
valeditor=find<CustomerEditor>(newScope)
//Docktheeditor
workspace.dock(editor)
}
16.Workspaces
225
That'salotofsteps.Fortunately,wecandothataswellinasinglecall:
funeditCustomer(customer:Customer){
workspace.dockInNewScope<CustomerEditor>(CustomerModel(customer))
}
The dockInNewScopefunctiontakesavararglistofinjectableobjectstoinsertintothenew
scopebeforelooking
upourCustomerEditoranddockingit.
Separatingscopesthiswaymakessurewecanutilizeinjectedviewmodelswithoutbeing
afraidofotherviewsstepping
onourdata.Itisapragmaticapproachtoanintricateproblem.Italsogivesyouawayof
bleedinginjectables
intonewscopes,shouldyourusecaserequireit.
CustomViewStackoptimizations
Someusecasesmightrequireyoutomakesurethattheusercannotgobacktoacertain
viewafterhehasnavigated
tothepriorview.YoucanremoveyourselffromtheViewStackonunDocklikethis:
overridefunonUndock(){
workspace.viewStack.remove(this)
}
Dockingmultipleviewsintheeditorarea
TheWorkspaceprovidesanalternativewaytonavigatebetweenviews.Insteadofbackand
forwardbuttons,youcanchoose
todockmultipleviewsinsideaTabPaneintheeditorarea.TheWorkspacehasa
navigationModepropertythatlets
youchangehowtheviewsarerepresentedintheeditorarea.Thedefaultis
Workspace.NavigationMode.Stack.Thefollowingexample
createsatabbedWorkspacethatautomaticallydockstwoviewsinsideitwhenit'screated:
16.Workspaces
226
classTabbedWorkspace:Workspace("TabbedWorkspace",NavigationMode.Tabs){
init{
dock<FirstView>()
dock<SecondView>()
}
}
Figure16.7
AWorkspaceinTabsmodeautomaticallyhidesthenavigationbuttonsastheyareno
longerneeded
YoucancreateastartingpointforthisWorkspacefromanormal Appclass:
classTabbedWorkspaceApp:App(TabbedWorkspace::class)
TheviewsdockedinsidetheWorkspacetabswillhavetheir onDockfunctioncalled
whenevertheyareaddedand
alsowhentheyaresubsequentlychosenastheactiveTab.Correspondingly,the onUndock
functioniscalled
wheneveritisnolongertheactiveTab,aswellaswhenit'sremovedfromtheTabPane
usingtheclosebuttononthetab.
16.Workspaces
227
YoucancontroltheclosablestateofaViewdockedinsidetheTabPaneviathe closeable
propertyin UIComponent.
Itreturnsa BooleanExpressionwiththedefaultvalueof truebutyoucanoverrideittobind
againstanother
propertyorsimplyreturnanother SimpleBooleanValue(false)tomakeituncloseable.This
examplemakessureyou
cannotclosethetabbeforetheCustomerModelinsideitiscommittedorrolledback:
classCustomerEditor:View("CustomerEditor"){
valcustomer:CustomerModelbyinject()
overridevalcloseable=customer.dirty.not()
}
Drawernavigation
TheWorkspacehasbuiltinsupportfortheDrawercontrol.Youcanaccess
workspace.leftDrawerand workspace.rightDrawerto
additemstoeachdrawer.Theywillshowuponeithertheleftorrightsidewheneveryou
haveaddedoneormoreitemstothem.
ItemsaddedfromaViewin onDockwillautomaticallyberemovedwhentheViewis
undocked.ItemsaddeddirectlyintheWorkspace
subclass,fromthe onBeforeShowAppcallbackorfromanyotherplacewillstayuntilthey
aremanuallyremoved.
Thecombinationofstaticanddynamicdraweritemsmakesforaverypowerfulnavigation
andmenustructure.Onlyyourimaginationisthelimit!
ThefollowingexamplecreatesacustomizeWorkspaceprimedwithadockedCustomer
Editorintheeditorareaandthethree
draweritemswecreatedintheDrawerchapterconfiguredstaticallyinthe leftDrawerofthe
Workspace:
16.Workspaces
228
//AFormbasedViewwewilldockintheworkspaceeditorarea
classCustomerEditor:View("CustomerEditor"){
overridevalroot=form{
fieldset(title){
field("Name"){textfield()}
field("Username"){textfield()}
button("Save")
}
}
}
classDrawerWorkspace:Workspace(){
init{
//DocktheCustomerEditorbydefault
dock<CustomerEditor>()
}
init{
//Additemstotheleftdrawers
with(leftDrawer){
item("Screencasts"){
webview{
prefWidth=470.0
engine.userAgent=iPhoneUserAgent
engine.load(TornadoFXScreencastsURI)
}
}
item("Links"){
listview(links){
cellFormat{link->
graphic=hyperlink(link.name).action{
hostServices.showDocument(link.uri)
}
}
}
}
item("People"){
tableview(people){
column("Name",Person::name)
column("Nick",Person::nick)
}
}
}
}
//Sampledataandconfigurationomittedforthisexample
}
InFigure16.8wehaveexpandedtheLinksdraweritem.NoticehowitpushestheCustomer
Editortotheright.
16.Workspaces
229
Figure16.8
Byrightclickingthedrawerandcheckingthe Floatingdrawersoption,theexpanded
draweritemcontentwill
insteadfloatabovethecontent,likeinFigure16.9:
Figure16.9
16.Workspaces
230
Thiscouldbeagoodideadependingontheavailablespaceandthenatureofthedocked
content.Youcanchangethe
floatingdrawermodeincodeaswell,bysetting leftDrawer.floatingDrawers=true.
RememberthatViewscancontributedraweritemsprogrammaticallyintheir onDock
callback.Usethisto
provideextratoolsforanadvancededitorforexample.Theycaneasilycommunicate
betweeneachother
usingViewModels.Itisrecommendedtocreateanewscopetomakeiteasierfortheseview
partstoworkinconcert
onshareddatastructures.
VetoingnavigationfromthedockedView
16.Workspaces
231
ThecurrentlydockedViewwillreceiveacallbackwhenevertheBackorForwardbuttonsof
theWorkspaceisclicked.Thesefunctions
arecalled onNavigateBackand onNavigateForward.Thedefaultimplementationreturnstrue
tosignalthatthenavigationshouldproceed.
Youcanhoweverreturnfalsetostopthenavigationandinsteadimplementyourownlogicto
decidewhathappensintheUIwhenoneofthe
navigationbuttonsareclicked.
16.Workspaces
232
Internationalization
TornadoFXmakesitveryeasytosupportmultiplelanguagesinyourapp.
InternationalizationinComponents
Each Componenthasaccesstoapropertycalled messagesoftype ResourceBundle.This
canbeusedtolookmessagesinthecurrentlocaleandassignthemtocontrols
programmatically:
classMyView:View(){
init{
valhelloLabel=Label(messages["hello"])
}
}
Alabelisprogrammaticallyconfiguredtogetit'stextfromaresourcebundle
Aswelloftheshorthandsyntax messages["key"],allotherfunctionsofthe ResourceBundle
classisavailableaswell.
Thebundleisautomaticallyloadedbylookingupabasenameequaltothefullyqualified
classnameofthe Component.ForaComponentnamed views.CustomerList,the
correspondingresourcebundlein /views/CustomerList.propertieswillbeused.Allnormal
variantsoftheresourcebundlenameissupported,seeResourceBundleJavadocsformore
information.
Internationalizationin FXML
Whenan FXMLfileisloadedviathe fxmldelegatefunction,thecorresponding messages
propertyofthecomponentwillbeusedinexactlythesameway.
<HBox>
<Labeltext="%hello"/>
</HBox>
Themessagewithkey hellowillbeinjectedintothelabel.
DefaultGlobalMessages
17.Internationalization
233
Youcanaddaglobalsetofmessageswiththebasename Messages(forexample
Messages_en.properties)attherootoftheclasspath.
Automaticlookupinparentbundle
Whenakeyisnotfoundinthecomponentbundle,orwhenthereisnobundleforthe
currrentcomponent,theglobalresourcebundleisconsulted.Assuch,youmightusethe
globalbundleforallresources,andplaceoverridesinthepercomponentbundle.
Friendlyerrormessages
Insteadofthrowinganexceptionwhenakeyisnotavailableinyourbundle,thevaluewill
simplybe [key].Thismakesiteasytospotyourerrors,andyourUIisstillfullyfunctional
whileyouaddthemissingkeys.
Configuringthelocale
Thedefaultlocaleistheoneretrievedfrom Locale.getDefault().Youcanconfigurea
differentlocalebyissuing:
FX.locale=Locale("my-locale")
Theglobalbundlewillautomaticallybechangedtothebundlecorrespondingtothenew
locale,andallsubsequentlyloadedcomponentswillgettheirbundleinthenewlocaleas
well.
Overridingresourcebundles
Ifyouwanttochangethebundleforacomponentafterit'sbeeninitialized,orifyousimply
wanttoloadaspesificbundlewithoutrelyingontheconventions,simplyassignthenew
bundletothe messagespropertyofthecomponent.
Ifyouwanttousetheoverridenresourcebundletoload FXML,makesureyouchangethe
bundlebeforeyouloadtherootview:
classMyView:View(){
init{messages=ResourceBundle.getBundle("MyCustomBundle")}
overridevalroot=HBoxbyfxml()
}
17.Internationalization
234
Amanuallyoverridenresourcebundleisusedbythe FXMLfilecorrespondingtothe
View
Thesametechniquecanbeusedtooverridetheglobalbundlebyassigningto
FX.messages.
Startuplocale
Youcanoverridethedefaultlocaleasearlyasthe Appclass initfunctionbyassigningto
FX.locale.
ControllersandFragmentsaswell
Thesameconventionsarevalidfor Controllersand Fragments,sincethefunctionalityis
madeavailabletotheircommonsuperclass, Component.
17.Internationalization
235
Configsettingsandstate
Savingapplicationstateisacommonrequirementfordesktopapps.TornadoFXhasseveral
featureswhichfacilitatessavingofUIstate,preferencesandgeneralappconfiguration
settings.
The confighelper
Eachcomponentcanhavearbitraryconfigurationsettingsthatwillbesavedaspropertyfiles
inafoldercalled confinsidethecurrentprogramfolder.
Belowisaloginscreenexamplewherelogincredentialsarestoredintheviewspecific
configobject.
18.ConfigSettingsandState
236
classLoginScreen:View(){
valloginController:LoginControllerbyinject()
valusername=SimpleStringProperty(this,"username",config.string("username"))
valpassword=SimpleStringProperty(this,"password",config.string("password"))
overridevalroot=form{
fieldset("Login"){
field("Username:"){textfield(username)}
field("Password:"){textfield(password)}
buttonbar{
button("Login").action{
runAsync{
loginController.tryLogin(username.value,password.value)
}ui{success->
if(success){
with(config){
set("username"tousername.value)
set("password"topassword.value)
save()
}
showMainScreen()
}
}
}
}
}
}
funshowMainScreen(){
//hideLoginScreenandshowthemainUIoftheapplication
}
}
Loginscreenwithcredentialsstoredintheviewspecificconfigobject
TheUIisdefinedwiththe TornadoFxtypesafebuilders,whichbasicallycontainsa form
withtwo TextField'sanda Button.Whentheviewisloaded,weassigntheusernameand
passwordvaluesfromtheconfigobject.Thesevaluesmightbenullatthispoint,ifnoprior
successfulloginwasperformed.Wethenbindthe usernameand passwordtothe
corresponding TextField's.
Lastbutnotleast,wedefinetheactionfortheloginbutton.Uponlogin,itcallsthe
loginController#tryLoginfunctionwhichtakestheusernameandpasswordfromthe
StringBindings(whichrepresenttheinputofthe TextFields),callsouttotheserviceand
returnstrueorfalse.
18.ConfigSettingsandState
237
Iftheresultistrue,weupdatetheusernameandpasswordintheconfigobjectandcalls
saveonit.Finally,wecall showMainScreenwhichcouldhidetheloginscreenandshowthe
mainscreenoftheapplication.
Pleasenotthattheexampleisnotabestpractiseforstoringsensitivedata,itmerely
illustrateshowyoucanusetheconfigobject.
Datatypesanddefaultvalues
configalsosupportsotherdatatypes.Itisanicepractisetowrapmultipleoperationson
theconfigobjectina withblock.
//Assigntox,defaultto50.0
varx=config.double("x",50.0)
varshowPrices=config.boolean("showPrices",boolean)
with(config){
set("x",root.layoutX)
set("showPrices",showPrices)
save()
}
Configurableconfigpath
The Appclasscanoverridethedefaultpathforconfigfilesbyoverriding configBasePath.
classMyApp:App(WelcomeView::class){
overridevalconfigBasePath:Paths.get("/etc/myapp/conf")
}
Thepathcanalsoberelative,whichmeansthepathwillbecreatedinsidethecurrent
workingdirectory.Bydefault,thebasepathis conf.
Overrideconfigpathpercomponent
Bydefault,afilecalled viewClass.propertiesiscreatedinsidethe configBasePath.This
canbeoverridenpercomponent:
classMyView:View(){
overridevalconfigPath=Paths.get("some/other/path/myview.properties")
18.ConfigSettingsandState
238
YoucanalsocreatetheViewspesificconfigfilebelowthe configBasePath,whichwould
makesenseinmostsituations.YoudothisbyaccessingtheAppclassthroughthe app
propertyoftheView.
classMyView:View(){
overridevalconfigPath=app.configBasePath.resolve("myview.properties")
Globalapplicationconfig
TheAppclassalsohasa configpropertyandacorresponding configPathproperty.By
default,theconfigurationfortheappclassisnamed app.config.Thiscanbeoverriddenthe
samewayyoudoforaViewconfig.
Theglobalconfigurationcanbeaccessedbyanycomponentatanytimeinthelifecycleof
theapplication.Simplyaccess app.configfromanywheretoreadorwriteyourglobal
configuration.
JSONconfigurationsettings
The configobjectsupports JsonObject, JsonArrayand JsonModel.Yousetthemusing
config.set("key"tovalue)andretrievethemusing config.jsonObject("key"),
config.jsonArray("key")and config.jsonModel("key").
The preferenceshelper
Asthe confighelperstorestheinformationinafoldercalled confpercomponent(view,
controller)the preferenceshelperwillsavesettingsintoanOSspecificway.InWindows
systemstheywillbestored HKEY_CURRENT_USER/Software/JavaSoft/....onMacosin
~/Library/Preferences/com.apple.java.util.prefs.plistandonLinuxsystemin ~/.java.
Wherethe confighelpersavespercomponent.The preferenceshelperismeanttobe
usedapplicationwide:
preferences("application"){
putBoolean("boolean",true)
putString("String","astring")
}
Retrievingpreferences:
18.ConfigSettingsandState
239
JSONandREST
JSONhasbecomethenewstandardfordataexchangeoverHTTP.WorkingwithJSONwith
thedatatypesdefinedin javax.jsonisnothard,butabitcumbersome.TheTornadoFX
JSONsupportcomes
intwoforms:Enhancementstothe javax.jsonobjectsandfunctionsandaspecialized
RESTclientthatdoesHTTPaswellasautomaticconversionbetweenJSONandyour
domainmodels.
TofacilitateconversionbetweentheseJSONobjectsandyourmodelobjects,youcan
choosetoimplementtheinterfaceJsonModelandoneorbothofthefunctions updateModel
and toJSON.
LaterinthischapterwewillintroducetheRESTclient,buttheJSONSupportcanalsobe
usedstandalone.TheRESTclientcallscertainfunctionsonJsonModelobjectsduringthe
lifecycleofanHTTPrequest.
updateModeliscalledtoconvertaJSONobjecttoyourdomainmodel.ItreceivesaJSON
objectfromwhichyoucanupdatethepropertiesofyourmodelobject.
toJSONiscalledtoconvertyourmodelobjecttoaJSONpayload.Itreceivesa
JsonBuilderwhereyoucansetthevaluesofthemodelobject.
classPerson:JsonModel{
varidbyproperty<Int>()
funidProperty()=getProperty(Person::id)
varfirstNamebyproperty<String>()
funfirstNameProperty()=getProperty(Person::firstName)
varlastNamebyproperty<String>()
funlastNameProperty()=getProperty(Person::lastName)
valphones=FXCollections.observableArrayList<Phone>()
overridefunupdateModel(json:JsonObject){
with(json){
id=int("id")
firstName=string("firstName")
lastName=string("lastName")
phones.setAll(getJsonArray("phones").toModel())
}
}
overridefuntoJSON(json:JsonBuilder){
19.JSONandREST
241
with(json){
add("id",id)
add("firstName",firstName)
add("lastName",lastName)
add("phones",phones.toJSON())
}
}
}
classPhone:JsonModel{
varidbyproperty<Int>()
funidProperty()=getProperty(Phone::id)
varnumberbyproperty<String>()
funnumberProperty()=getProperty(Phone::number)
overridefunupdateModel(json:JsonObject){
with(json){
id=int("id")
number=string("number")
}
}
overridefuntoJSON(json:JsonBuilder){
with(json){
add("id",id)
add("number",number)
}
}
}
JsonModelwithgetters/settersandproperty()accessorfunctionstobeJavaFX
Propertycompatible
Whenyouimplement JsonModelyoualsogetthe copyfunction,whichcreatesacopyof
yourmodelobject.
TornadoFXalsocomeswithspecialsupportfunctionsforreadingandwritingJSON
properties.PleaseseethebottomofJson.ktforanexhaustivelist.
AlltheJSONretrievalfunctionsacceptsavarargargumentforthekeyintheJSON
document.Thefirstkeyavailableinthedocumentwillbeusedtoretrievethevalue.This
makesiteasiertoworkwithslightlyinconsistentJSONschemesorcanbeusedasaternary
toprovideafallbackvalueforexample.
Configuringdatetime
19.JSONandREST
242
The datetime(key)functionusedtoretrievea LocalDateTimeobjectfromJSONwillby
defaultexpectavalueof"Secondssinceepoch".Ifyourexternalwebserviceexpects
"Millisecondssinceepoch"instead,
youcaneithersend datetime(key,millis=true)orconfigureitgloballybysetting
JsonConfig.DefaultDateTimeMillis=true.
GeneratingJSONobjects
The JsonBuilderisanabstractionover javax.json.JsonObjectBuilderthatsupportsnull
values.Insteadofblowingup,itsilentlydismissesthemissingentry,whichenablesyouto
buildyourJSONobjectgraph
morefluentlywithoutcheckingfornulls.
RESTClient
TheRESTClientthatmakesiteasytoperformJSONbasedRESTcalls.Theunderlying
HTTPengineinterfacehastwoimplementations.ThedefaultusesHttpURLConnectionand
thereisalsoanimplementationbasedonApacheHttpClient.Itiseasytoextendthe
Rest.Enginetosupportotherhttpclientlibrariesifneeded.
TousetheApacheHttpClientimplementation,simplycall Rest.useApacheHttpClient()inthe
initmethodofyourAppclassandincludethe org.apache.httpcomponents:httpclient
dependencyinyourprojectdescriptor.
Configuration
Ifyoumostlyaccessthesameapioneverycall,youcansetabaseurisosubsequentcalls
onlyneedtoincluderelativeurls.Youcanconfigurethebaseurlanywhereyoulike,butthe
initfunctionofyour Appclassisagoodplacetodoit.
classMyApp:App(){
valapi:Restbyinject()
init{
api.baseURI="http://contoso.com/api"
}
}
19.JSONandREST
243
Basicoperations
Thereareconveniencefunctionstoperform GET, PUT, POSTand DELETEoperations.
classCustomerController:Controller(){
valapi=Restbyinject()
funloadCustomers():ObservableList<Customer>=
api.get("customers").list().toModel()
}
CustomerControllerwithloadCustomerscall
So,whatexactlyisgoingoninthe loadCustomersfunction?Firstwecall
api.get("customers")whichwillperformthecallandreturna Responseobject.Wethen
call Response.list()whichwillconsumetheresponseandconvertittoa
javax.json.JsonArray.Lastly,wecalltheextensionfunction JsonArray.toModel()which
createsone Customerobjectper JsonObjectinthearrayandcalls JsonModel.updateModel
onit.Inthisexample,thetypeargumentistakenfromthefunctionreturntype,butyoucould
alsowritetheabovemethodlikethisifyouprefer:
funloadCustomers()=api.get("customers").list().toModel<Customer>()
Howyouprovidethetypeargumenttothe toModelfunctionisamatteroftaste,sochoose
thesyntaxyouaremostcomfortablewith.
Thesefunctionstakeanoptionalparameterwitheithera JsonObjectora JsonModelthat
willbethepayloadofyourrequest,convertedtoaJSONstring.
Thefollowingexampleupdatesacustomerobject.
funupdateCustomer(customer:Customer)=api.put("customers/${customer.id}",customer)
Iftheapiendpointreturnsthecustomerobjecttousaftersave,wewouldfetchaJsonObject
bycalling one()andthen toModel()toconvertitbackintoourmodelobject.
funupdateCustomer(customer:Customer)=
api.put("customers/${customer.id}",customer).one().toModel<Customer>()
Queryparameters
19.JSONandREST
244
QueryparametersneedstobeURLencoded.The Map.queryStringextensionvaluewill
turnanymapintoaproperlyURLencodedquerystring:
valparams=mapOf("id"to1)
api.put("customers${params.queryString}",customer).one().toModel<Customer>()
ThiswillcalltheURI customers?id=1.
Errorhandling
IfanI/Oerroroccursduringtheprocessingoftherequest,thedefaultErrorHandlerwill
reporttheerrortotheuser.Youcanofcoursecatchanyerrorsyourselfinstead.Tohandle
HTTPreturncodes,youmightwanttoinspectthe Responsebeforeyouconverttheresultto
JSON.Makesureyoualwayscall consume()ontheresponseifyoudon'textractdatafrom
itusinganyofthemethods list(), one(), text()or bytes().
fungetCustomer(id:Int):Customer{
valresponse=api.get("some/action")
try{
if(response.ok())
returnresponse.one().toModel()
elseif(response.statusCode==404)
throwCustomerNotFound()
else
throwMyException("getCustomerreturned${response.statusCode}${response.
reason}")
}finally{
response.consume()
}
}
Extractstatuscodeandreasonfrom HttpResponse
response.ok()isshorthandfor response.statusCode==200.
Authentication
TornadoFXmakesitveryeasytoaddbasicauthenticationtoyourapirequests:
api.setBasicAuth("username","password")
19.JSONandREST
245
Toconfigureauthenticationmanually,configurethe requestInterceptoroftheenginetoadd
customheadersetctotherequest.Forexample,thisishowthebasicauthenticationis
implementedforthe HttpUrlEngine:
requestInterceptor={request->
valb64=Base64.getEncoder().encodeToString("$username:$password".toByteArray(UTF
_8))
request.addHeader("Authorization","Basic$b64")
}
Foramoreadvancedexampleofconfiguringtheunderlyingclient,takealookathowbasic
authenticationisimplementedinthe HttpClientEngine.setBasicAuthfunctioninRest.kt.
Interceptingcalls
YoucanforexampleshowaloginscreenifanHTTPcallfailswithstatusCode401:
api.engine.responseInterceptor={response->
if(response.statusCode==401)
showLoginScreen("Invalidcredentials,pleaseloginagain.")
}
Settingtimeouts
Youcanconfigurethereadtimeoutforthedefaultproviderbyusinga requestInterceptor
andcastingtherequestto HttpURLRequestbeforeyooperateonit.
api.engine.requestInterceptor={
(itasHttpURLRequest).connection.readTimeout=5000
}
Youcanconfigurethe connectionTimeoutofthe HTTPUrlConnectionobjectaboveinthe
sameway.
ConnecttomultipleAPI's
Youcancreatemultipleinstancesofthe Restclassbysubclassingitandconfiguringeach
subclassasyouwish.Injectionofsubclassesworkseamlessly.Overridethe engine
propertyifyouwanttouseanotherenginethanthedefault.
19.JSONandREST
246
DefaultenginefornewRestinstances
TheengineusedbyanewRestclientisconfiguredwiththe engineProvideroftheRest
class.Thisiswhathappenswhenyoucall Rest.useApacheHttpClient:
Rest.engineProvider={rest->HttpClientEngine(rest)}
The engineProviderreturnsaconcrete engineimplementationthatisgiventhe
current Restinstanceasargument.
Youcanoverridetheconfigured engineina Restinstanceatanytime.
Proxy
Aproxycanbeconfiguredeitherbyimplementinganinterceptorthataugmentseachcall,or,
preferablyonceperRestclientinstance:
rest.proxy=Proxy(Proxy.Type.HTTP,InetSocketAddress("127.0.0.1",8080))
Sequencenumbers
Ifyoudomultiplehttpcallstheywillnotbepooledandreturnedintheorderyouexecuted
thecalls.Anyhttprequestwillreturnassoonasitisavailable.Ifyouwanttohandlethemin
sequence,orevendiscardolderresults,youcanusethe Response.seqvaluewhichwill
containa Longsequencenumber.
Progressindicator
TornadoFXcomeswithaHTTPProgressIndicatorView.Thisviewcanbeembeddedinyour
applicationandwillshowyouinformationaboutongoingRESTcalls.Embedthe
RestProgressBarintoaToolBaroranyotherparentcontainer:
toolbar.add(RestProgressBar::class)
19.JSONandREST
247
DependencyInjection
TODO:ThispageissnippedfromtheWikiandneedsarewrite
Viewand Controlleraresingletons,soyouneedsomewaytoaccesstheinstanceofa
specificcomponent.TornadoFXsupportsdependencyinjection,butyoucanalsolookup
componentswiththe findfunction.
valmyController=find(MyController::class)
Whenyoucall find,thecomponentcorrespondingtothegivenclassislookedupina
globalcomponentregistry.Ifitdidnotexistpriortothecall,itwillbecreatedandinserted
intotheregistrybeforethefunctionreturns.
Ifyouwanttodeclarethecontrollerreferanceasafieldmemberhowever,youshoulduse
the injectdelegateinstead.Thisisalazymechanism,sotheactualinstancewillonlybe
createdthefirsttimeyoucallafunctionontheinjectedresource.Using injectisalways
prefered,asitallowsyourcomponentstohavecirculardependencies.
valmyController:MyControllerbyinject()
Thirdpartyinjectionframeworks
TornadoFXmakesiteasytoinjectresourcesfromathirdpartydependencyinjection
framework,likeforexampleGuiceorSpring.Allyouhavetodoisimplementtheverysimple
DIContainerinterfacewhenyoustartyourapplication.Let'ssayyouhaveaGuicemodule
configuredwithafictive HelloService.StartGuiceinthe initblockofyour Appclass
andregisterthemodulewithTornadoFX:
valguice=Guice.createInjector(MyModule())
FX.dicontainer=object:DIContainer{
overridefun<T:Any>getInstance(type:KClass<T>)
=guice.getInstance(type.java)
}
TheDIContainerimplementationisconfiguredtodelegatelookupsto
guice.getInstance
20.DependencyInjection
248
Toinjectthe HelloServiceconfiguredin MyModule,usethe didelegateinsteadofthe
injectdelegate:
valMyView:View(){
valhelloService:HelloServicebydi()
}
The didelegateacceptsanybeantype,while injectwillonlyallowbeansoftype
Injectable,whichincludesTornadoFX's Viewand Controller.Thiskeepsaclean
separationbetweenyourUIbeansandanybeansconfiguredintheexternaldependency
injectionframework.
SettingupforSpring
AbovethesetupforGuiceisshown.SettingupforSpring,inthiscaseusing beans.xmlas
ApplicationContextisdoneasfollows:
beans.xml
<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scanbase-package="no.tornadofx.fxsample.springexample"/>
<context:annotation-config/>
</beans>
ThissetsSpringuptoscanforbeans.
Applicationstartup
20.DependencyInjection
249
classSpringExampleApp:App(SpringExampleView::class){
init{
valspringContext=ClassPathXmlApplicationContext("beans.xml")
FX.dicontainer=object:DIContainer{
overridefun<T:Any>getInstance(type:KClass<T>):T=springContext.get
Bean(type.java)
}
}
}
ThisinitializedthespringcontextandhooksitintotornadoFXviathe FX.dicontainer.Now
youcaninjectSpringbeanslikethis:
valhelloBean:HelloBeanbydi()
ItisquitecommonintheSpringworldtonameabeanlikeso:
<beanid="helloWorld"class="com.tutorialspoint.HelloWorld">
<propertyname="message"value="HelloWorld!"/>
</bean>
Thebeanisthenaccessibleusingthe id.ThiscanbedoneintornadoFXtoo:
classSpringExampleApp:App(SpringExampleView::class){
init{
valspringContext=ClassPathXmlApplicationContext("beans.xml")
FX.dicontainer=object:DIContainer{
overridefun<T:Any>getInstance(type:KClass<T>):T=springContext.get
Bean(type.java)
overridefun<T:Any>getInstance(type:KClass<T>,name:String):T=spr
ingContext.getBean(type.java,name)
}
}
}
Thesecond getInstanceusesboththetypeofthebeanandtheidofthebean.
Instantiatingabeanisdownas:
valhelloBean:HelloBeanbydi("helloWorld")
20.DependencyInjection
250
Wizard
Sometimesyouneedtoasktheuserforalotofinformationandaskingforitallatonce
wouldresultinatoocomplexuserinterface.Perhapsyoualsoneedtoperformcertain
operationswhileorafteryouhaverequestedtheinformation.
Forthesesituations,youcanconsiderusingawizard.Awizardtypicallyhastwoormore
pages.Itletstheusernavigatebetweenthepagesaswellascompleteorcancelthe
process.
TornadoFXhasapowerfulandcustomizableWizardcomponentthatletsyoudojustthat.In
thefollowingexampleweneedtocreateanewCustomerandwehavedecidedtoaskfor
thebasiccustomerinfoonthefirstpageandtheaddressinformationonthenext.
Let'shavealookattwosimpleinputViewsthatgathersaidinformationfromtheuser.The
BasicDatapageasksforthenameofthecustomerandthetypeofcustomer(Personor
Company).Bynowyoucanprobably CustomerModelguesshowthe Customerand
CustomerModelobjectslook,sowewon'trepeatthemhere.
21.Wizard
251
classBasicData:View("BasicData"){
valcustomer:CustomerModelbyinject()
overridevalroot=form{
fieldset(title){
field("Type"){
combobox(customer.type,Customer.Type.values().toList())
}
field("Name"){
textfield(customer.name).required()
}
}
}
}
classAddressInput:View("Address"){
valcustomer:CustomerModelbyinject()
overridevalroot=form{
fieldset(title){
field("Zip/City"){
textfield(customer.zip){
prefColumnCount=5
required()
}
textfield(customer.city).required()
}
}
}
}
Bythemselves,theseviewsdon'tdomuch,butputtogetherinaWizardwestarttoseehow
powerfulthisinputparadigmcanbe.OurinitialWizardcodeisonlythis:
classCustomerWizard:Wizard("Createcustomer","Providecustomerinformation"){
valcustomer:CustomerModelbyinject()
init{
graphic=resources.imageview("/graphics/customer.png")
add(WizardStep1::class)
add(WizardStep2::class)
}
}
TheresultcanbeseeninFigure21.1.
21.Wizard
252
Figure21.1
JustbylookingattheWizardtheusercanseewhathewillbeaskedtoprovide,howhecan
navigatebetweenthepagesandhowtocompleteorcanceltheprocess.
SincetheWizarditselfisbasicallyjustanormal View,itwillrespondtothe openModalcall.
Let'simagineabuttonthatopenstheWizard:
button("AddCustomer").action{
find<CustomerWizard>{
openModal()
}
}
Pagenavigation
Bydefault,the Backand Nextbuttonsareavailablewhenevertherearemorepages
eitherpreviousornextinthewizard.
For Nextnavigationhowever,whetherthewizardactuallynavigatestothenextpageis
dependentuponthe completedstateofthecurrentpage.Every Viewhasa completed
propertyandacorresponding isCompletedvariableyoucanmanipulate.
21.Wizard
253
Whenthe Nextor Finishbuttonisclicked,the onSavefunctionofthecurrentpageis
called,andthenavigationactionisonlyperformedifthecurrentpage's completedvalueis
true.Every Viewiscompletedbydefault,that'swhywecannavigatetopagenumbertwo
withoutcompletingpageonefirst.Let'schangethat.
Inthe BasicDataeditor,weoverridethe onSavefunctiontoperformapartialcommitofthe
nameand typefields,becausethat'stheonlytwofieldstheusercanchangeonthatpage.
overridefunonSave(){
isComplete=customer.commit(customer.name,customer.type)
}
Thecommitfunctionnowcontrolsthecompletedstateofourwizardpage,hencecontroller
whethertheuserisallowedtonavigatetotheaddresspage.Ifwetrytonavigatewithout
fillinginthename,wewillbegrantedbythevalidationerrormessageinFigure21.2:
Figure21.2
Wecouldgoontodothesamefortheaddresseditor,takingcaretoonlycommitthe
editablefields:
overridefunonSave(){
isComplete=customer.commit(customer.zip,customer.city)
}
21.Wizard
254
IftheuserclickstheFinishbutton,the onSavefunctionintheWizarditselfisactivated.Ifthe
Wizard's completedstateistrueafterthe onSavecall,thewizarddialogisclosed,provided
thattheusercalls super.onSave().Insuchascenario,theWizarditselfneedstohandle
whatevershouldhappeninthe onSavefunction.Anotherpossibilityistoconfigurea
callbackthatwillbeexecutedwheneverthewizardiscompleted.Withthatapproach,we
needaccessthecompletedcustomerobjectsomehow,soweinjectitintothewizarditselfas
well:
classCustomerWizard:Wizard(){
valcustomer:CustomerModelbyinject()
}
Let'srevisitthebuttonactionthatactivatedthewizardandaddan onCompletecallbackthat
extractsthecustomerandinsertsitintoadatabasebeforeitopensthenewlycreated
CustomerobjectinaCustomerEditorView:
button("AddCustomer").action{
find<CustomerWizard>{
onComplete{
runAsync{
database.insert(customer.item)
}ui{
workspace.dockInNewScope<CustomerEditor>(customer.item)
}
}
openModal()
}
}
Wizardscoping
Inourexample,bothoftheWizardpagesshareacommonviewmodel,namelythe
CustomerModel.Thismodelisinjectedintobothpages,soitshouldbethesameinstance.
Butwhatifotherpartsoftheapplicationisalreadyusingthe CustomerModelinthesame
scopewecreatedtheWizardfrom?Itturnsoutthatthisisnotevenanissue,becausethe
Wizardbaseclassimplements InjectionScopedwhichmakessurethatwheneveryou
injecta Wizardsubclass,anewscopeisautomaticallyactivated.Thismakessurethat
whateverresourceswerequireinsidetheWizardwillbeuniqueandnotsharedwithany
otherpartoftheapplication.
ItalsomeansthatifyouneedtoinjectexistingdataintoaWizard'sscope,youmustdoso
manually:
21.Wizard
255
valwizard=find<MyWizard>()
wizard.scope.set(someExistingObject)
wizard.openModal()
Improvingthevisualcues
Ununtilnow,the Nextbuttonwasenabledwhenevertherewasanotherpagetonavigate
forwardto.The Finishbuttonwasalsoalwaysenabled.Thismightbefine,butyoucan
improvethecuesgiventoyourusersbyonlyenablingthosebuttonswhenitwouldmake
sensetoclickthem.Bylookingintothe Wizardbaseclass,wecanseethatthebuttonsare
boundtothefollowingbooleanexpressions:
openvalcanFinish:BooleanExpression=SimpleBooleanProperty(true)
openvalcanGoNext:BooleanExpression=hasNext
The canFinishexpressionisboundtothe Finishbuttonandthe canGoNextexpressionis
boundtothe Nextbutton.The Wizardclassalsoincludessomebooleanexpressionsthat
areunusedbydefault.Twoofthoseare currentPageCompleteand allPagesComplete.These
expressionsarealwaysuptodate,andwecanusetheminour CustomerWizardtoimprove
theuserexperience.
classCustomerWizard:Wizard(){
overridevalcanFinish=allPagesComplete
overridevalcanGoNext=currentPageComplete
}
Withthisredefinitioninplace,the Nextand Finishbuttonswillonlybeenabledwhenever
thenewconditionsaremet.Thisiswhatwewant,butwe'renotdoneyet.Rememberhow
weonlyupdated isCompletedwhenever onSavewascalled?Youmightalsoremember
that onSavewascalledwhenever Nextor Finishwasclicked?Itlookslikewehave
ourselvesagoodoldCatch22situationhere,folks!
Thesolutionishoweverquitesimple:Insteadofevaluatingthecompletedstateonsave,we
willdoitwheneverachangeismadetoanyofourinputfields.Weneedtomakesurethat
wesupplythe autocommitparametertoeachbindinginourViewModel:
21.Wizard
256
classCustomerModel:ItemViewModel<Customer>(){
valname=bind(Customer::nameProperty,autocommit=true)
valzip=bind(Customer::zipProperty,autocommit=true)
valcity=bind(Customer::cityProperty,autocommit=true)
valtype=bind(Customer::typeProperty,autocommit=true)
}
Theinputfieldsinourwizardpagesareboundtotheseproperties,andwheneverachange
ismade,theunderlyingCustomerobjectwillbeupdated.Wenolongerneedtocall
customer.commit()inour onSavecallback,butwedoneedtoredefinethe complete
booleanexpressionineachwizardpage.
Hereisthenewdefinitioninthe BasicDataView:
overridevalcomplete=customer.valid(customer.name)
Andhereisthedefinitioninthe AddressInputView:
overridevalcomplete=customer.valid(customer.street,customer.zip,customer.city)
Webindthecompletedstateofourwizardpagestoaneverupdatingbooleanexpression
whichindicateswhethertheeditablepropertiesforthatpageisvalidornot.
Remembertodeletethe onSavefunctionsaswenolongerneedthem.Ifyourunthe
applicationwiththesechangesyouwillseehowmuchmoreexpressivetheWizardbecomes
intermsoftellingtheuserwhenhecanproceedandwhenhecanfinishtheprocess.Using
thisapproachwillalsoconveythatanynon-filleddataisoptionaloncethe Finishbuttonis
enabled.
Hereisthecompletelyrewrittenwizardandpages:
21.Wizard
257
classCustomerWizard:Wizard(){
valcustomer:CustomerModelbyinject()
overridevalcanGoNext=currentPageComplete
overridevalcanFinish=allPagesComplete
init{
add(BasicData::class)
add(AddressInput::class)
}
}
classBasicData:View("BasicData"){
valcustomer:CustomerModelbyinject()
overridevalcomplete=customer.valid(customer.name)
overridevalroot=form{
fieldset(title){
field("Type"){
combobox(customer.type,Customer.Type.values().toList())
}
field("Name"){
textfield(customer.name).required()
}
}
}
}
classAddressInput:View("Address"){
valcustomer:CustomerModelbyinject()
overridevalcomplete=customer.valid(customer.zip,customer.city)
overridevalroot=form{
fieldset(title){
field("Zip/City"){
textfield(customer.zip){
prefColumnCount=5
required()
}
textfield(customer.city).required()
}
}
}
}
Stylingandadaptingthelookandfeel
21.Wizard
258
Therearemanybuiltinoptionsyoucanconfiguretochangethelookandfeelofthewizard.
Commonforthemallisthattheyhaveobservable/writablepropertieswhichyoucanbindto
overjustsetinyourwizardsubclass.Foreachaccessorbelowtherewillbeacorresponding
accessorProperty.
Modifyingthestepsindicator
Steps
Thestepslistisontheleftofthewizard.Ithasthefollowingconfigurationoptions:
Name Description
showSteps Setto falsetoremovethestepsviewcompletely
stepsText Changetheheaderfrom"Steps"toanydesiredString
showStepsHeader Removetheheader
enableStepLinks Setto truetoturneachstepdescriptionintoahyperlink
stepLinksCommits Setto falsetonolongerrequirethatthecurrentpageisvalid
beforenavigatingtothenewpage
numberedSteps Setto truetoaddtheindexnumberbeforeeachstep
description
Navigation
YoucanchangethetextofthenavigationbuttonsandcontrolnavigationflowwithEnter:
Name Description
backButtonText Changethetextofthe Backbutton
nextButtonText Changethetextofthe Nextbutton
cancelButtonText Changethetextofthe Cancelbutton
finishButtonText Changethetextofthe Finishbutton
enterProgresses Entergoestonextpagewhencompleteandfinishonlastpage
Headerarea
Name Description
showHeader Setto falsetoremovetheheader
graphic Anodethatwillshowuponthefarrightoftheheader
21.Wizard
259
Structuralmodifications
Therootofthe Wizardclassisa BorderPane.Theheaderwillbeinthe topslot,thesteps
areinthe leftslot,thepagesareinthe centerslotandthebuttonsareinthe bottom
slot.Youcanchange/hide/addstylingandsetpropertiestothesenodesasyouseefitto
alterthedesignandlayoutoftheWizard.Agoodplacetodothiswouldbeinthe onDock
callbackofyourwizardsubclass.Itiscompletelyvalidchangethelayoutinanywayyousee
fit,youcanevenremovethe BorderPaneandmovetheotherpartsintoanotherlayout
containerforexample.
21.Wizard
260
AppendixA-Reference
A1-PropertyDelegates
Kotlinispackedwithgreatlanguagefeatures,anddelegatedpropertiesareapowerfulway
tospecifyhowapropertyworksandcreatere-usablepoliciesforthoseproperties.Ontopof
theonesthatexistinKotlin'sstandardlibrary,TornadoFXprovidesafewmoreproperty
delegatesthatareparticularlyhelpfulforJavaFXdevelopment.
SingleAssign
Itisoftenidealtoinitializepropertiesimmediatelyuponconstruction.Butinevitablythereare
timeswhenthissimplyisnotfeasible.Whenapropertyneedstodelayitsinitializationuntilit
isfirstcalled,alazydelegateistypicallyused.Youspecifyalambdainstructinghowthe
propertyvalueisinitializedwhenitsgetteriscalledthefirsttime.
valfooValuebylazy{buildExpensiveFoo()}
Buttherearesituationswherethepropertyneedstobeassignedlaternotbyavalue-
supplyinglambda,butrathersomeexternalentityatalatertime.Whenweleveragetype-
safebuilderswemaywanttosavea Buttontoaclass-levelpropertysowecanreferenceit
later.Ifwedonotwant myButtontobenullable,weneedtousethe lateinitmodifier.
classMyView:View(){
lateinitvarmyButton:Button
overridevalroot=vbox{
myButton=button("NewEntry")
}
}
Theproblemwith lateinitisitcanbeassignedmultipletimesbyaccident,anditisnot
necessarilythreadsafe.Thiscanleadtoclassicbugsassociatedwithmutability,andyou
reallyshouldstriveforimmutabilityasmuchaspossible(EffectiveJavabyBloch,Item#13).
Byleveragingthe singleAssign()delegate,youcanguaranteethatpropertyisonly
assignedonce.Anysubsequentassignmentattemptswillthrowaruntimeerror,andsowill
accessingitbeforeavalueisassigned.Thiseffectivelygivesustheguaranteeof
AppendixA-SupplementaryTopics
261
immutability,althoughitisenforcedatruntimeratherthancompiletime.
classMyView:View(){
varmyButton:ButtonbysingleAssign()
overridevalroot=vbox{
myButton=button("NewEntry")
}
}
Eventhoughthissingleassignmentisnotenforcedatcompiletime,infractionscanbe
capturedearlyinthedevelopmentprocess.Especiallyascomplexbuilderdesignsevolve
andvariableassignmentsmovearound, singleAssign()isaneffectivetooltomitigate
mutabilityproblemsandallowflexibletimingforpropertyassignments.
Bydefault, singleAssign()synchronizesaccesstoitsinternalvalue.Youshouldleaveit
thiswayespeciallyifyourapplicationismultithreaded.Ifyouwishtodisablesynchronization
forwhateverreason,youcanpassa SingleAssignThreadSafetyMode.NONEvalueforthepolicy.
varmyButton:ButtonbysingleAssign(SingleAssignThreadSafetyMode.NONE)
JavaFXPropertyDelegate
DonotconfusetheJavaFX PropertywithastandardJava/Kotlin"property".The Property
isaspecialtypein JavaFXthatmaintainsavalueinternallyandnotifieslistenersofits
changes.ItisproprietarytoJavaFXbecauseitsupportsbindingoperations,andwillnotify
theUIwhenitchanges.The PropertyisacorefeatureofJavaFXandhasitsown
JavaBeans-likepattern.
Thispatternisprettyverbosehowever,andevenwithKotlin'ssyntaxefficienciesitstillis
prettyverbose.Youhavetodeclarethetraditionalgetter/setteraswellasthe Propertyitem
itself.
classBar{
privatevalfooPropertybylazy{SimpleObjectProperty<T>()}
funfooProperty()=fooProperty
varfoo:T
get()=fooProperty.get()
set(value)=fooProperty.set(value)
}
AppendixA-SupplementaryTopics
262
Fortunately,TornadoFXcanabstractmostofthisaway.BydelegatingaKotlinpropertytoa
JavaFX property(),TornadoFXwillget/setthatvalueagainstanew Propertyinstance.To
followJavaFX'sconventionandprovidethe PropertyobjecttoUIcomponents,youcan
createafunctionthatfetchesthe PropertyfromTornadoFXandreturnsit.
classBar{
varfoobyproperty<String>()
funfooProperty()=getProperty(Bar::foo)
}
Especiallyasyoustartworkingwith TableViewandothercomplexcontrols,youwilllikely
findthispatternhelpfulwhencreatingmodelclasses,andthispatternisusedinseveral
placesthroughoutthisbook.
Noteyoudonothavetospecifythegenerictypeifyouhaveaninitialvaluetoprovidetothe
property.Inthebelowexample,itwillinferthetypeas`String.
classBar{
varfoobyproperty("baz")
funfooProperty()=getProperty(Bar::foo)
}
AlternativePropertySyntax
Thereisalsoanalternativesyntaxwhichproducesalmostthesameresult:
importtornadofx.getValue
importtornadofx.setValue
classBar{
valfooProperty=SimpleStringProperty()
varfoobyfooProperty
}
HereyoudefinetheJavaFXpropertymanuallyanddelegatethegettersandsettersdirectly
fromtheproperty.Thismightlookcleanertoyou,andsoyouarefreetochoosewhatever
syntaxyouaremostcomfortablewith.However,thefirstalternativecreatesaJavaFX
compliantpropertyinthatitexposesthe Propertyviaafunctioncalled fooProperty(),
whilethelattersimplyexposesavariablecalled fooProperty.ForTornadoFXthereisno
difference,butifyouinteractwithlegacylibrariesthatrequireapropertyfunctionyoumight
needtostickwiththefirstone.
FXMLDelegate
AppendixA-SupplementaryTopics
263
Ifyouhaveagiven MyViewViewwithaneighboringFXMLfile MyView.fxmldefiningthe
layout,the fxid()propertydelegatewillretrievethecontroldefinedintheFXMLfile.The
controlmusthavean fx:idthatisthesamenameasthevariable.
<Labelfx:id="counterLabel">
Nowwecaninjectthis Labelintoour Viewclass:
valcounterLabel:Labelbyfxid()
Otherwise,theIDmustbespecificallypassedtothedelegatecall.
valmyLabel:Labelbyfxid("counterLabel")
PleasereadChapter10tolearnmoreaboutFXML.
A2-TableViewAdvancedColumnResizing
TheSmartResizepolicybringstheabilitytointuitivelyresizecolumnsbyprovidingsensible
defaultscombinedwithpowerfulanddynamicconfigurationoptions.
Toapplytheresizepolicytoa TableViewweconfigurethe columnResizePolicy.Forthis
exercisewewillusealistofhotelrooms.Thisisourinitialtablewiththe SmartResizepolicy
activated:
tableview(rooms){
column("#",Room::id)
column("Number",Room::number)
column("Type",Room::type)
column("Bed",Room::bed)
columnResizePolicy=SmartResize.POLICY
}
HereisapictureofthetablewiththeSmartResizepolicyactivated(Figure5.7):
FigureA2.1
AppendixA-SupplementaryTopics
264
Thedefaultsettingsgaveeachcolumnthespaceitneedsbasedonitscontent,andgave
theremainingwidthtothelastcolumn.Whenyouresizeacolumnbydraggingthedivider
betweencolumnheaders,onlythecolumnimmediatelytotherightwillbeaffected,which
avoidspushingthecolumnstotherightoutsidetheviewportofthe TableView.
Whilethisoftenpresentsapleasantdefault,thereisalotmorewecandotoimprovethe
userexperienceinthisparticularcase.Itisevidentthatourtabledidnotneedthefull800
pixelsitwasprovided,butitgivesusanicechancetoelaborateontheconfigurationoptions
ofthe SmartResizepolicy.
Thebedcolumniswaytoobig,anditseemsmoresensibletogivetheextraspacetothe
Typecolumn,sinceitmightcontainarbitrarylongdescriptionsoftheroom.Togivetheextra
spacetotheTypecolumn,wechangeitscolumndefinition(Figure5.8):
column("Type",Room::type).remainingWidth()
FigureA2.2
AppendixA-SupplementaryTopics
265
NowitisapparenttheBedcolumnlookscramped,beingpushedallthewaytotheleft.We
configureittokeepitsdesiredwidthbasedonthecontentplus50pixelspadding:
column("Bed",Room:bed").contentWidth(padding=50.0)
Theresultisamuchmorepleasantvisualimpression(Figure5.9):
FigureA2.3
Thisfine-tuningmaynotseemlikeabigdeal,butitmeansalottopeoplewhoareforcedto
stareatyoursoftwareallday!Itisthelittlethingsthatmakesoftwarepleasanttouse.
AppendixA-SupplementaryTopics
266
IftheuserincreasesthewidthoftheNumbercolumn,theTypecolumnwillgradually
decreaseinwidth,untilitreachesitsdefaultwidthof10pixels(theJavaFXdefault).After
that,theBedcolumnmuststartgivingawayitsspace.Wedon'teverwanttheBedcolumn
tobesmallerthatwhatweconfigured,sowetellittouseitscontent-basedwidthplusthe
paddingweaddedasitsminimumwidth:
column("Bed",Room:bed").contentWidth(padding=50.0,useAsMin=true)
TryingtodecreasetheBedcolumneitherbyexplicitlyexpandingtheTypecolumnor
implicitlybyexpandingtheNumbercolumnwillsimplybedeniedbytheresizepolicy.Itis
worthnotingthatthereisalsoa useAsMaxchoiceforthe contentWidthresizetype.This
wouldeffectivelyresultinahard-coded,unresizablecolumn,basedontherequiredcontent
widthplusanyconfiguredpadding.Thiswouldbeagoodpolicyforthe#column:
column("#",Room::id).contentWidth(useAsMin=true,useAsMax=true)
Therestoftheexampleswillprobablynotbenefittheuser,buttherearestillotheroptionsat
yourdisposal.TrytomaketheNumbercolumn25%ofthetotaltablewidth:
column("Number",Room::number).pctWidth(25.0)
Whenyouresizethe TableView,theNumbercolumnwillgraduallyexpandtokeepupwith
our25%widthrequirement,whiletheTypecolumngetstheremainingextraspace.
FigureA2.4
AppendixA-SupplementaryTopics
267
Analternativeapproachtopercentagewidthistospecifyaweight.Thistimeweaddweights
tobothNumberandType:
column("Number",Room::number).weigthedWidth(1.0)
column("Type",Room::type).weigthedWidth(3.0)
Thetwoweightedcolumnssharetheremainingspaceaftertheothercolumnshavereceived
theirfairshare.SincetheTypecolumnhasaweightthatisthreetimesbiggerthanthe
Numbercolumn,itssizewillbethreetimesbiggeraswell.Thiswillbereevaluatedasthe
TableViewitselfisresized.
FigureA2.5
Thissettingwillmakesurewekeepthementionedratiobetweenthetwocolumns,butit
mightbecomeproblematicifthe TableViewisresizedtobeverysmall.ThetheNumber
columnwouldnothavespacetoshowallofitscontent,soweguardagainstthatby
specifyingthatitshouldnevergrowbelowthespaceitneedstoshowitscontent,plussome
padding,forgoodmeasure:
column("Number",Room::number).weigthedWidth(1.0,minContentWidth=true,padding=10
.0)
Thismakessureourtablebehavesnicelyalsounderconstrainedwidthconditions.
Dynamiccontentresizing
AppendixA-SupplementaryTopics
268
Sincesomeoftheresizingmodesarebasedontheactualcontentofthecolumns,they
mightneedtobereevaluatedevenwhenthetableorit'scolumnsaren'tresized.For
example,ifyouaddorremovecontentitemsfromthebackinglist,therequiredcontent
measurementsmightneedtobeupdated.Forthisyoucancallthe requestResizefunction
afteryouhavemanipulatedtheitems:
SmartResize.POLICY.requestResize(tableView)
Infact,youcanasktheTableViewtoaskthepolicyforyou:
tableView.requestResize()
Staticallysettingthecontentwidth
Inmostcasesyouprobablywanttoconfigureyourcolumnwidthsbasedoneitherthetotal
availablespaceorthecontentofthecolumns.Insomecasesyoumightwanttoconfigurea
specificwidth,thatthatcanbedonewiththe prefWidthfunction:
column("Bed",Room::bed).prefWidth(200.0)
Acolumnwithapreferredwidthcanberesized,sotomakeitnon-resizable,usethe
fixedWidthfunctioninstead:
column("Bed",Room::bed).fixedWidth(200.0)
Whenyouhard-codethewidthofthecolumnsyouwillmostlikelyendupwithsomeextra
space.Thisspacewillbeawardedtotherightmostresizablecolumn,unlessyouspecify
remainingWidth()foroneormorecolumn.Inthatcase,thesecolumnswilldividetheextra
spacebetweenthem.
Inthecasewherenotallcolumnscanbeaffordedtheirpreferredwidth,allresizable
columnsmustgiveawaysomeoftheirspace,butthe SmartResizePolicymakessurethat
thecolumnwiththebiggestreductionpotentialwillgiveawayitsspacefirst.Thereduction
potentialisthedifferencebetweenthecurrentwidthofthecolumnanditsdefinedminimum
width.
AppendixA-SupplementaryTopics
269
AppendixB-ToolsandUtilities
LayoutDebugger
Whenyou'recreatinglayoutsorworkingonCSSitsometimeshelptobeabletovisualise
thescenegraphandmakelivechangestothenodepropertiesofyourlayout.The
absolutelybesttoolforthisjobisdefinitelytheScenicViewtoolfromFXExperience,but
sometimesyoujustneedtogetaquickoverviewasfastaspossible.
Debuggingascene
SimplyhitAlt-Meta-JtobringupthebuiltindebuggingtoolLayoutDebugger.Thedebugger
attachestothecurrentlyactive Sceneandopensanewwindowthatshowsyouthecurrent
scenegraphandpropertiesforthecurrentlyselectednode.
Usage
WhilethedebuggerisactiveyoucanhoveroveranynodeinyourViewanditwillbe
automaticallyhighlightedinthedebuggerwindow.Clickinganodewillalsoshowyouthe
propertiesofthatnode.Someofthepropertiesareeditable,like backgroundColor, text,
paddingetc.
Whenyouhoveroverthenodetreeinthedebugger,thecorrespondingnodeisalso
highlighteddirectlyintheView.
AppendixB-ToolsandUtilities
270
Stopadebuggingsession
Closethedebuggerwindowbyhitting Escandthedebuggersessionends.Youcandebug
multiplescenessimultaneously,eachdebuggingsessionwillopenanewwindow
correspondingtothesceneyoudebug.
Configurableshortcut
Thedefaultshortcutforthedebuggercanbechangedbysettinganinstanceof
KeyCodeCombinationinto FX.layoutDebuggerShortcut.Youcanevenchangetheshortcut
whiletheappisrunning.Agoodplacetoconfiguretheshortcutwouldbeinthe initblock
ofyour Appclass.
Addingfeatures
WhilethisdebuggertoolisinnowayareplacementforScenicView,wewilladdfeatures
basedonreasonablefeaturerequests.Ifthefeatureaddsvalueforsimpledebugging
purposesandcanbeimplementedinasmallamountofcode,wewilltrytoaddit,orbetter
AppendixB-ToolsandUtilities
271
yet,submitapullrequest.Havealookatthesourcecodetofamiliariseyourselfwiththe
tool.
TODO
Therearealotofutilitiesandmiscellaneous"oddsandends"inTornadoFX.Somehowwe
needtoorganizeandgroupallofthemintoafewchapters.Hereistheproposedoutline.
Feelfreetomakeeditstothisdocumentuntilweareallhappywiththedirection.
Thisisabitchallengingtoorganizebecausesomeoftheseutilitiesarehelpfulbutoftenhard
tocategorize.WecanalwaysthrowitemsintotheAppendixiftheydonotfitanywhere.
Enteringfullscreen
Toenterfullscreenyouneedtogetaholdofthecurrent stageandcall stage.isFullScreen
=true.Theprimarystageistheactivestageunlessyouopenedamodalwindowvia
view.openModal()ormanuallycreatedastage.Theprimarystageisavailableinthe
variable FX.primaryStage.Toopentheapplicationinfullscreenonstartupyoushould
override startinyourappclass:
classMyApp:App(MyView::class){
overridefunstart(stage:Stage){
super.start(stage)
stage.isFullScreen=true
}
}
Inthefollowingexamplewetogglefullscreenmodeinamodalwindowviaabutton:
button("Togglefullscreen"){
setOnAction{
with(modalStage){isFullScreen=!isFullScreen}
}
}
10.ConcurrencyandErrorHandling
AsyncTaskExecution
AsyncItemsforDataComponents
FXRunandWait
ErrorHandler
AppendixB-ToolsandUtilities
272
11.DataTools
JsonModal
RESTClient
SortedFilteredList
MentionSortedFilteredList.refilter()whichcausestheexistingpredicatetobe
reevaluated
Clipboard
Resources
12.Configuration
ProgramParametersandHotViewReloading
Internationalization
ComponentConfiguration
Preferences
13.JavaInterop
POJOBinding
JavaFXInterop
14.TornadoFXPlugin
ConfiguringanApplication
AddView
InjectComponent
Intentions
ConvertfieldmemberstoJavaFXProperties
AddTableViewColumns
ProjectTemplates
Appendix
ThirdPartyInjection
Logging
ListofExtensionFunctions
AppendixB-ToolsandUtilities
273
Logging
Componenthasalazyinitializedinstanceof java.util.Loggernamed log.Usage:
log.info{"Logmessagehere"}
TornadoFXmakesnochangestotheloggingcapabilitiesof java.util.Logger.Seethe
javadocformoreinformation.
Buildtools
Maven
TheTornadofxpluginforIntellijcancrete3typesofmavenprojects.
1. Astandardmavenproject
2. AnOSGIenabledmavenproject
3. AnOSGIenabledmavenprojectwheretheviewsareexposeddeclarativly.
Allthreeprojectscreateaworkingandcompilableprojectthatcanserveasastartingpoint.
Ifyoudonotwanttousetheplugintocreatetheprojectthatthepom.xmlneedstohaveata
minimum:
<properties>
<kotlin.version>1.1.1</kotlin.version>
<tornadofx.version>1.7.0</tornadofx.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
AppendixB-ToolsandUtilities
274
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
....
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>no.tornado</groupId>
<artifactId>tornadofx</artifactId>
<version>${tornadofx.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
AppendixB-ToolsandUtilities
275
Note
StartingwithTornadoFXversion1.7.1youmustconfigurethekotlincompilerplugintotarget
jvmTarget1.8asintheexampleabove.
AppendixB-ToolsandUtilities
276