table of contents · eventbus 16. workspaces 17. internationalization 18. config settings and state...

276
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 Table of Contents 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

Upload: others

Post on 15-Mar-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

  • 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

    TableofContentsIntroduction

    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

  • TornadoFXGuideThisisacontinualefforttofullydocumenttheTornadoFXframeworkintheformatofabook.

    Introduction

    2

    https://github.com/edvin/tornadofx

  • IntroductionUserinterfacesarebecomingincreasinglycriticaltothesuccessofconsumerandbusinessapplications.Withtheriseofconsumermobileappsandwebapplications,businessusersareincreasinglyholdingenterpriseapplicationstoahigherstandardofquality.Theywantrich,feature-packeduserinterfacesthatprovideimmediateinsightandnavigatecomplexscreensintuitively.Moreimportantly,theywanttheapplicationtoadaptquicklytobusinesschangesonafrequentbasis.Forthedeveloper,thismeanstheapplicationmustnotonlybemaintainablebutalsoevolvable.TornadoFXseekstoassistalltheseobjectivesandgreatlystreamlinethecodingofJavaFXUI's.

    WhilemuchoftheenterpriseITworldispushingHTML5andcloud-basedapplications,manybusinessesarestillusingdesktopUIframeworkslikeJavaFX.Whileitdoesnotdistributetolargeaudiencesaseasilyaswebapplications,JavaFXworkswellfor"in-house"businessapplications.Itshigh-performancewithlargedatasets(andthefactitisnativeJava)makeitapracticalchoiceforapplicationsusedbehindthecorporatefirewall.

    JavaFX,likemanyUIframeworks,canquicklybecomeverboseanddifficulttomaintain.Fortunately,thereleaseofKotlinhascreatedanopportunitytorethinkhowJavaFXapplicationsarebuilt.

    AninterestingproductthatisindevelopmentisJPro,aweb-basedJavaFXcontainerthatusesnoplugins.ItcanworkwithTornadoFXandJavaFX,butisstillinclosedbetaatthetimeofwriting.Youcanfollowtheprojectandwaitforitsavailabilityhere:https://jpro.io/

    WhyTornadoFX?InFebruary2016,JetBrainsreleasedKotlin,anewJVMlanguagethatemphasizespragmatismoverconvention.KotlinworksatahigherlevelofabstractionandprovidespracticallanguagefeaturesnotavailableinJava.OneofthemoreimportantfeaturesofKotlinisits100%interoperabilitywithexistingJavalibrariesandcodebases,includingJavaFX.

    WhileJavaFXcanbeusedwithKotlininthesamemannerasJava,somebelievedKotlinhadlanguagefeaturesthatcouldstreamlineandsimplifyJavaFXdevelopment.WellbeforeKotlin'sbeta,EugenKissprototypedJavaFX"builders"withKotlinFX.InJanuary2016,EdvinSyserebootedtheinitiativeandreleasedTornadoFX.

    1.WhyTornadoFX?

    3

    https://jpro.io/http://kotlinlang.org

  • TornadoFXseekstogreatlyminimizetheamountofcodeneededtobuildJavaFXapplications.Itnotonlyincludestype-safebuilderstoquicklylayoutcontrolsanduserinterfaces,butalsofeaturesdependencyinjection,delegatedproperties,controlextensionfunctions,andotherpracticalfeaturesenabledbyKotlin.TornadoFXisafineshowcaseofhowKotlincansimplifycodebases,andittacklestheverbosityofUIcodewitheleganceandsimplicity.ItcanworkinconjunctionwithotherpopularJavaFXlibrariessuchasControlsFXandJFXtras.ItworksespeciallywellwithreactiveframeworkssuchasReactFXaswellasRxJavaandfriends(includingRxJavaFX,RxKotlin,andRxKotlinFX).

    ReaderRequirementsThisbookexpectsreaderstohavesomeknowledgeofKotlinandhavespentsometimegettingacquaintedwithit.TherewillbesomecoverageofKotlinlanguagefeaturesbutonlytoacertainextent.Ifyouhavenotdonesoalready,readtheJetBrainsKotlinReferenceandspendagoodfewhoursstudyingit.

    ItdefinitelyhelpstobefamiliarwithJavaFXbutitisnotarequirement.PerhapsyoustartedstudyingJavaFXbutfoundthedevelopmentexperiencetobetedious,andyouarecheckingoutTornadoFXhopingitprovidesabetterwaytobuilduserinterfaces.IfthisdescribesyourexperienceandyouarelearningKotlin,thenyouwillprobablybenefitfromthisguide.

    AMotivationalExampleIfyouhaveworkedwithJavaFXbefore,youmighthavecreatedaTableViewatsomepoint.SayyouhaveagivendomaintypePerson.TornadoFXallowsyoutomuchmoreconciselycreatetheJavaBeans-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"containingaTableViewwithasmallcodefootprint.

    1.WhyTornadoFX?

    4

    http://fxexperience.com/controlsfx/http://jfxtras.org/https://github.com/TomasMikula/ReactFXhttps://github.com/ReactiveX/RxJavahttps://github.com/ReactiveX/RxJavaFXhttps://github.com/ReactiveX/RxKotlinhttps://github.com/thomasnield/RxKotlinFXhttps://kotlinlang.org/docs/reference/

  • 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!IfyouhoneinonjustthepartdeclaringtheTableViewwithfourcolumns(shownbelow),youwillseeittookasimplefunctionalconstructtobuildaTableView.Itwillautomaticallysupporteditstothefieldsaswell.

    tableview(persons){column("ID",Person::idProperty)column("Name",Person::nameProperty)column("Birthday",Person::birthdayProperty)column("Age",Person::age)}

    Asshownbelow,wecanusethecellFormat()extensionfunctiononaTableColumn,andcreateconditionalformattingfor"Age"valuesthatarelessthan18.

    1.WhyTornadoFX?

    5

  • tableview{items=personscolumn("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,andTornadoFXispackedwithexpressivepowerfordozensofcaseslikethis.ThisallowsyoutofocusoncreatingsolutionsratherthanengineeringUIcode.YourJavaFXapplicationswillnotonlybeturnedaroundmorequickly,butalsobemaintainableandevolvable.

    1.WhyTornadoFX?

    6

  • SettingUpTouseTornadoFX,thereareseveraloptionstosetupthedependencyforyourproject.MainstreambuildautomationtoolslikeGradleandMavenaresupportedandshouldhavenoissuesingettingsetup.

    PleasenotethatTornadoFXisaKotlinlibrary,andthereforeyourprojectneedstobeconfiguredtouseKotlin.ForGradleandMavenconfigurations,pleaserefertotheKotlinGradleSetupandKotlinMavenSetupguides.MakesureyourdevelopmentenvironmentorIDEisequippedtoworkwithKotlinandhastheproperpluginsandcompilers.

    ThisguidewilluseIntellijIDEAtowalkthroughcertainexamples.IDEAistheIDEofchoicetoworkwithKotlin,althoughEclipsehasapluginaswell.

    GradleForGradle,youcansetupthedependencydirectlyfromMavenCentral.Providethedesiredversionnumberforthex.y.zplaceholder.

    repositories{mavenCentral()}

    //MinimumjvmTargetof1.8neededsinceKotlin1.1compileKotlin{kotlinOptions.jvmTarget=1.8}

    dependencies{compile'no.tornado:tornadofx:x.y.z'}

    MavenToimportTornadoFXwithMaven,addthefollowingdependencytoyourPOMfile.Providethedesiredversionnumberforthex.y.zplaceholder.

    Goesintokotlin-maven-pluginblock:

    2.SettingUp

    7

    http://gradle.org/https://maven.apache.org/https://kotlinlang.org/docs/reference/using-gradle.htmlhttps://kotlinlang.org/docs/reference/using-maven.html

  • 1.8

    Thenthisgoesintodependenciesblock:

    no.tornadotornadofxx.y.z

    OtherBuildAutomationSolutions

    ForinstructionsonhowtouseTornadoFXwithotherbuildautomationsolutions,pleaserefertothe[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")

    ManualImportTomanuallydownloadandimporttheJARfile,gototheTornadoFXreleasepageortheCentralRepository.DownloadtheJARfileandconfigureitintoyourproject.

    StartingaTornadoFXApplicationNewerversionsoftheJVMknowhowtostartJavaFXapplicationswithoutamain()method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthatextendsjavafx.application.Application.Sincetornadofx.Appextendsjavafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwouldstarttheappbyreferencingcom.example.app.MyApp,andyoudon'tnecessarilyneedamain()functionunlessyouneedtosupplycommandlinearguments.InthatcaseyouwouldaddapackagelevelmainfunctiontotheMyApp.ktfile:

    funmain(args:Array){Application.launch(MyApp::class.java,*args)}

    2.SettingUp

    8

    http://search.maven.org/#search|gav|1|g%3A"no.tornado]%28http://search.maven.org/#search|gav|1|g%3A"no.tornadohttps://github.com/edvin/tornadofx/releaseshttps://search.maven.org

  • Thismainfunctionwouldbecompiledtocom.example.app.MyAppKt-noticetheKtattheend.Whenyoucreateapackagelevelmainfunction,itwillalwayshaveaclassnameofthefullyqualifiedpackage,plusthefilename,appendedwithKt.

    Infact,TornadoFXalsocontainsahelpertomakeitevennicertolaunchapplicationsfromamainclassbyacceptingtheappclassasagenerictypeparameter:

    funmain(args:Array)=launch(args)

    2.SettingUp

    9

  • ComponentsJavaFXusesatheatricalanalogytoorganizeanApplicationwithStageandScenecomponents.TornadoFXbuildsonthisbyprovidingView,Controller,andFragmentcomponents.WhiletheStage,andSceneareusedbyTornadoFX,theView,Controller,andFragmentintroducesnewconceptsthatstreamlinedevelopment.Manyofthesecomponentsareautomaticallymaintainedassingletons,andcancommunicatetoeachotherthroughsimpledependencyinjectionsandothermeans.

    YoualsohavetheoptiontoutilizeFXMLwhichwillbediscussedmuchlater.Butfirst,letsextendApptocreateanentrypointthatlaunchesaTornadoFXapplication.

    AppandViewBasicsTocreateaTornadoFXapplication,youmusthaveatleastoneclassthatextendsApp.AnAppistheentrypointtotheapplicationandspecifiestheinitialView.ItdoesinfactextendJavaFXApplication,butyoudonotnecessarilyneedtospecifyastart()ormain()method.

    Butfirst,extendApptocreateyourownimplementationandspecifytheprimaryviewasthefirstconstructorargument.

    classMyApp:App(MyView::class)

    AViewcontainsdisplaylogicaswellasalayoutofNodes,similartotheJavaFXStage.Itisautomaticallymanagedasasingleton.WhenyoudeclareaViewyoumustspecifyarootpropertywhichcanbeanyNodetype,andthatwillholdtheView'scontent.

    InthesameKotlinfileorinanewfile,extendaclassoffofView.OverridetheabstractrootpropertyandassignitVBoxoranyotherNodeyouchoose.

    classMyView:View(){overridevalroot=VBox()}

    However,wemightwanttopopulatethisVBoxactingastherootcontrol.Usingtheinitializerblock,let'saddaJavaFXButtonandaLabel.Youcanusethe"plusassign"+=operatorstoaddchildren,suchasaButtonandLabel

    3.Components

    10

    https://kotlinlang.org/docs/reference/classes.html#constructors

  • classMyView:View(){overridevalroot=VBox()

    init{root+=Button("PressMe")root+=Label("")}}

    Whileitisprettyclearwhat'sgoingonfromlookingatthiscode,TornadoFXprovidesabuildersyntaxthatwillstreamlineyourUIcodefurtherandmakeitmucheasiertoreasonabouttheresultingUIjustbylookingatthecode.Wewillgraduallymoveintobuildersyntax,andfinallycoverbuildersinfullinthenextchapter.

    Whileweintroduceyoutonewconcepts,youmightsometimesseecodethatisnotusingbestpractices.Wedothistointroduceyougraduallytoconceptsandgiveyouabroaderunderstandingofwhatisgoingonunderthehood.Graduallywewillintroducemorepowerfulconstructstosolvetheproblemathandinabetterway.

    Nextwewillseehowtorunthisapplication.

    StartingaTornadoFXApplicationNewerversionsoftheJVMknowhowtostartJavaFXapplicationswithoutamain()method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthatextendsjavafx.application.Application.Sincetornadofx.Appextendsjavafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwouldstarttheappbyreferencingcom.example.app.MyApp,andyoudonotnecessarilyneedamain()functionunlessyouneedtosupplycommandlinearguments.InthatcaseyouwouldaddapackagelevelmainfunctiontotheMyApp.ktfile:

    funmain(args:Array){Application.launch(MyApp::class.java,*args)}

    Thismainfunctionwouldbecompiledtocom.example.app.MyAppKt.NoticetheKtattheend.Whenyoucreateapackagelevelmainfunction,itwillalwayshaveaclassnameofthefullyqualifiedpackage,plusthefilename,appendedwithKt.

    ForlaunchingandtestingtheApp,wewilluseIntellijIDEA.NavigatetoRun→EditConfigurations(Figure3.1).

    Figure3.1

    3.Components

    11

  • Clickthegreen"+"signandcreateanewApplicationconfiguration(Figure3.2).

    Figure3.2

    3.Components

    12

  • Specifythenameofyour"Mainclass"whichshouldbeyourAppclass.Youwillalsoneedtospecifythemoduleitresidesin.Givetheconfigurationameaningfulnamesuchas"Launcher".Afterthatclick"OK"(Figure3.3).

    Figure3.3

    YoucanrunyourTornadoFXapplicationbyselectingRun→Run'Launcher'orwhateveryounamedtheconfiguration(Figure3.4).

    Figure3.4

    3.Components

    13

  • Youshouldnowseeyourapplicationlaunch(Figure3.5)

    Figure3.5

    Congratulations!Youhavewrittenyourfirst(albeitsimple)TornadoFXapplication.Itmaynotlooklikemuchrightnow,butaswecovermoreofTornadoFX'spowerfulfeatureswewillbecreatinglarge,impressiveuserinterfaceswithlittlecodeinnotime.Butfirstlet'sunderstandalittlebetterwhatishappeningbetweenAppandView.

    UnderstandingViewsLet'sdivealittledeeperintohowaViewworksandhowitcanbeused.TakealookattheAppandViewclasseswejustbuilt.

    3.Components

    14

  • classMyApp:App(MyView::class)

    classMyView:View(){overridevalroot=VBox()

    init{with(root){this+=Button("PressMe")this+=Label("Waiting")}}}

    AViewcontainsahierarchyofJavaFXNodesandisinjectedbynamewhereveritiscalled.InthenextsectionwewilllearnhowtoleveragepowerfulbuilderstocreatetheseNodehierarchiesquickly.ThereisonlyoneinstanceofMyViewmaintainedbyTornadoFX,effectivelymakingitasingleton.TornadoFXalsosupportsscopes,whichcangrouptogetheracollectionofViews,FragmentsandControllersinseparateinstances,resultinginaViewonlybeingasingletoninsidethatscope.ThisisgreatforMultiple-DocumentInterfaceapplicationsandotheradvancedusecases.Thisiscoveredinalaterchapter.

    Usinginject()andEmbeddingViewsYoucanalsoinjectoneormoreViewsintoanotherView.BelowweembedaTopViewandBottomViewintoaMasterView.Noteweusetheinject()delegatepropertytolazilyinjecttheTopViewandBottomViewinstances.Thenwecalleach"child"View'sroottoassignthemtotheBorderPane(Figure3.6).

    classMasterView:View(){valtopView:TopViewbyinject()valbottomView:BottomViewbyinject()

    overridevalroot=borderpane{top=topView.rootbottom=bottomView.root}}

    classTopView:View(){overridevalroot=label("TopView")}

    classBottomView:View(){overridevalroot=label("BottomView")}

    3.Components

    15

    https://msdn.microsoft.com/en-us/library/aa263481.aspx

  • Figure3.6

    IfyouneedViewstocommunicatetoeachother,youcancreateapropertyineachofthe"child"Viewsthatholdsthe"parent"View.

    classMasterView:View(){overridevalroot=BorderPane()

    valtopView:TopViewbyinject()valbottomView:BottomViewbyinject()

    init{with(root){top=topView.rootbottom=bottomView.root}

    topView.parent=thisbottomView.parent=this}}

    classTopView:View(){overridevalroot=Label("TopView")lateinitvarparent:MasterView}

    classBottomView:View(){overridevalroot=Label("BottomView")lateinitvarparent:MasterView}

    MoretypicallyyouwoulduseaControlleroraViewModeltocommunicatebetweenviews,andwewillvisitthistopiclater.

    InjectionUsingfind()Theinject()delegatewilllazilyassignagivencomponenttoaproperty.Thefirsttimethatcomponentiscallediswhenitwillberetrieved.Alternatively,insteadofusingtheinject()delegateyoucanusethefind()functiontoretrieveasingletoninstanceofaVieworothercomponents.

    3.Components

    16

  • classMasterView:View(){overridevalroot=BorderPane()

    valtopView=find(TopView::class)valbottomView=find(BottomView::class)

    init{with(root){top=topView.rootbottom=bottomView.root}}}

    classTopView:View(){overridevalroot=Label("TopView")}

    classBottomView:View(){overridevalroot=Label("BottomView")}

    Youcanuseeitherfind()orinject(),butusinginject()delegatesisthepreferredmeanstoperformdependencyinjection.

    IntroductiontoBuildersWhilewewillcoverbuildersmoreindepthinthenextchapter,itistimetorevealthattheaboveexamplecanbewritteninamuchmoreconciseandexpressivesyntax:

    classMasterView:View(){overridevalroot=borderpane{top(TopView::class)bottom(BottomView::class)}}

    InsteadofinjectingtheTopViewandBottomViewandthenassigningtheirrespectiverootnodestotheBorderPanestopandbottomproperty,wespecifytheBorderPanewiththebuildersyntax(alllowercase)andthendeclarativelytellTornadoFXtopullinthetwosubviewsandassignthemtothetopandbottompropertiesautomatically.Hopefullyyouagreethisismuchmoreexpressive,withalotlessboilerplate.ThisisoneofthemostimportantprinciplesTornadoFXtriestoliveby:Reduceboilerplateandincreasereadability.Theendresultisoftenlesscodeandlessbugs.

    3.Components

    17

  • ControllersInmanycases,itisconsideredagoodpracticetoseparateaUIintothreedistinctparts:

    1. Model-Thebusinesscodelayerthatholdscorelogicanddata2. View-Thevisualdisplaywithvariousinputandoutputcontrols3. Controller-The"middleman"mediatingeventsbetweentheModelandtheView

    ThereareotherflavorsofMVClikeMVVMandMVP,allofwhichcanbeleveragedinTornadoFX.

    WhileyoucouldputalllogicfromtheModelandControllerrightintotheview,itisoftencleanertoseparatethesethreepiecesdistinctlytomaximizereusability.OnecommonlyusedpatterntoaccomplishthisistheMVCpattern.InTornadoFX,aControllercanbeinjectedtosupportaView.

    Hereisasimpleexample.CreateasimpleViewwithaTextFieldwhosevalueiswrittentoa"database"whenaButtonisclicked.WecaninjectaControllerthathandlesinteractingwiththemodelthatwritestothedatabase.Sincethisexampleissimplified,therewillbenodatabasebutaprintedmessagewillserveasaplaceholder(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,wemakesuretoaddareferencetotheinputFieldsothatitcanbereferencesfromtheonClickeventhandlerofthe"Commit"buttonlater.Whenthe"Commit"buttonisclicked,youwillseetheControllerprintsalinetotheconsole.

    WritingAlphatodatabase!

    Itisimportanttonotethatwhiletheaboveworks,andmayevenlookprettygood,itisagoodpracticetoavoidreferencingotherUIelementsdirectly.YourcodewillbemucheasiertorefactorifyoubindyourUIelementstopropertiesandmanipulatethepropertiesinstead.WewillintroducetheViewModellater,whichprovideseveneasierwaystodealwiththistypeofinteraction.

    YoucanalsouseControllerstoprovidedatatoaView(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

  • TheVBoxcontainsaLabelandaListView,andtheitemspropertyoftheListViewisassignedtothevaluespropertyofourController.

    Whethertheyarereadingorwritingdata,Controllerscanhavelong-runningtasksandshouldnotperformworkontheJavaFXthread.YouwilllearnhowtoeasilyoffloadworktoaworkerthreadusingtherunAsyncconstructlaterinthischapter.

    Longrunningtasks

    Wheneveryoucallafunctioninacontrolleryouneedtodetermineifthatfunctionreturnsimmediatelyorifitperformspotentiallylong-runningtasks.IfyoucallafunctionontheJavaFXApplicationThread,theUIwillbeunresponsiveuntilthecallcompletes.UnresponsiveUI'sisakillerforuseracceptance,somakesurethatyourunexpensiveoperationsinthebackground.TornadoFXprovidestherunAsyncfunctiontohelpwiththis.

    CodeplacedinsidearunAsyncblockwillruninthebackground.IftheresultofthebackgroundcallshouldupdateyourUI,youmustmakesurethatyouapplythechangesontheJavaFXApplicationThread.Theuiblockdoesexactlythat.

    3.Components

    20

  • valtextfield=textfield()button("Updatetext"){action{runAsync{myController.loadText()}ui{loadedText->textfield.text=loadedText}}}

    Whenthebuttonisclicked,theactioninsidetheactionbuilder(whichdelegatestheActionEventtosetActionmethod)isrun.ItmakesacallouttomyController.loadText()andappliestheresulttothetextpropertyofthetextfieldwhenitreturns.TheUIstaysresponsivewhilethecontrollerfunctionruns.

    Underthecovers,runAsynccreatesJavaFXTaskobjects,andspinsoffaseparatethreadtorunyourcallinsidetheTask.YoucanassignthisTasktoavariableandbindittoaUItoshowprogresswhileyouroperationisrunning.

    Infact,thisissocommonthatthereisalsoandefaultViewModelcalledTaskStatuswhichcontainsobservablevaluesforrunning,message,title,andprogress.YoucansupplytherunAsynccallwithaspecificinstanceoftheTaskStatusobject,orusethedefault.

    TheTornadoFXsourcesincludesanexampleusageofthisintheAsyncProgressApp.ktfile.

    ThereisalsoaversionofrunAsynccalledrunAsyncWithProgresswhichwillcoverthecurrentnodewithaprogressindicatorwhilethelongrunningoperationruns.

    singleAssign()PropertyDelegate

    IntheexampleaboveweinitializedtheinputFieldpropertywiththesingleAssigndelegate.Ifyouwanttoguaranteethatavalueisonlyassignedonce,youcanusethesingleAssign()delegateinsteadofthelateinitkeywordfromKotlin.Thiswillcauseasecondassignmenttothrowanerror,anditwillalsoerrorwhenitisprematurelyaccessedbeforeitisassigned.

    YoucanlookupmoreaboutsingleAssign()indetailinAppendixA1,butknowfornowitguaranteesavarcanonlybeassignedonce.Itisalsothreadsafeandhelpsmitigateissueswithmutability.

    Fragment

    3.Components

    21

  • AnyViewyoucreateisasingleton,whichmeansyoutypicallyuseitinonlyoneplaceatatime.ThereasonforthisisthattherootnodeoftheViewcanonlyhaveasingleparentinaJavaFXapplication.Ifyouassignitanotherparent,itwilldisappearfromitspreviousparent.

    However,ifyouwouldliketocreateapieceofUIthatisshort-livedorcanbeusedinmultipleplaces,considerusingaFragment.AFragmentisaspecialtypeofViewthatcanhavemultipleinstances.TheyareparticularlyusefulforpopupsoraspiecesofalargerUI(suchasListCells,whichwelookatviatheListCellFragmentlater).

    BothViewandFragmentsupportopenModal(),openWindow()andopenInternalWindow()thatwillopentherootnodeinaseparateWindow.

    classMyView:View(){overridevalroot=vbox{button("PressMe"){action{find(MyFragment::class).openModal(stageStyle=StageStyle.UTILITY)}}}}

    classMyFragment:Fragment(){overridevalroot=label("Thisisapopup")}

    YoucanpassoptionalargumentstoopenModal()aswelltomodifyafewofitsbehaviors.

    OptionalArgumentsforopenModal()

    Argument Type Description

    stageStyle StageStyle DefinesoneofthepossibleenumstylesforStage.Default:StageStyle.DECORATED

    modality ModalityDefinesoneofthepossibleenummodalitytypesforStage.Default:Modality.APPLICATION_MODAL

    escapeClosesWindow Boolean SetstheESCkeytocallcloseModal().Default:true

    owner Window SpecifytheownerWindowforthisStage`

    block Boolean BlockUIexecutionuntiltheWindowcloses.Default:false

    InternalWindow

    3.Components

    22

  • WhileopenModalopensinanewStage,openInternalWindowopensoverthecurrentrootnode,oranyothernodeifyouspecifyit:

    button("Openeditor"){action{openInternalWindow(Editor::class)}}

    Figure3.9

    AgoodusecasefortheinternalwindowisforsinglestageenvironmentslikeJPro,orifyouwanttocustomizethewindowtrimtomakethewindowappearmoreinlinewiththedesignofyourapplication.TheInternalWindowcanbestyledwithCSS.TakealookattheInternalWindow.Stylesclassformoreinformationaboutstyleableproperties.

    TheinternalwindowAPIdiffersfrommodal/windowinoneimportantaspect.Sincethewindowopensoveranexistingnode,youtypicallycallopenInternalWindow()fromwithintheViewyouwantittoopenontopof.YousupplytheViewyouwanttoshow,andyoucanoptionallysupplywhatnodetoopenoverviatheownerparameter.

    OptionalArgumentsforopenInternalWindow()

    3.Components

    23

  • Argument Type Description

    view UIComponent Thecomponentwillbethecontentofthenewwindow

    view KClass Alternatively,youcansupplytheclassoftheviewinsteadofaninstance

    icon Node Optionalwindowicon

    scope Scope Ifyouspecifytheviewclass,youcanalsospecifythescopeusedtofetchtheview

    modal BooleanDefinesifthecoveringnodeshouldbedisabledwhiletheinternalwindowisactive.Default:true

    escapeClosesWindow Boolean SetstheESCkeytocallclose().Default:true

    owner NodeSpecifytheownerNodeforthiswindow.Thewindowwillbydefaultcovertherootnodeofthisview.`

    ClosingmodalwindowsAnyComponentopenedusingopenModal(),openWindow()oropenInternalWindow()canbeclosedbycallingcloseModal().ItisalsopossibletogettotheInternalWindowinstancedirectlyifneededusingfindParentOfType(InternalWindow::class).

    ReplacingViewsandDockingEventsWithTornadoFX,iseasytoswapyourcurrentViewwithanotherViewusingreplaceWith(),andoptionallyaddatransition.Intheexamplebelow,aButtononeachViewwillswitchtotheotherview,whichcanbeMyView1orMyView2(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

    YoualsohavetheoptiontospecifyaspiffyanimationforthetransitionbetweenthetwoViews.

    replaceWith(MyView1::class,ViewTransition.Slide(0.3.seconds,Direction.LEFT)

    ThisworksbyreplacingtherootNodeongivenViewwithanotherView'sroot.TherearetwofunctionsyoucanoverrideonViewtoleveragewhenaView'srootNodeisconnectedtoaparent(onDock()),andwhenitisdisconnected(onUndock()).Youcanleveragethesetwoeventstoconnectand"cleanup"wheneveraViewcomesinorfallsout.YouwillnoticerunningthecodebelowthatwheneveraViewisswapped,itwillundockthatpreviousViewanddockthenewone.Youcanleveragethesetwoeventstomanageinitializationanddisposaltasks.

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

    PassingparameterstoviewsThebestwaytopassinformationbetweenviewsisoftenaninjectedViewModel.Evenso,itcanstillbeconvenienttobeabletopassparameterstoothercomponents.ThefindandinjectfunctionssupportsvarargsofPairwhichcanbeusedforjustthispurpose.Consideracustomerlistthatopensacustomereditorfortheselectedcustomer.Theactiontoeditacustomermightlooklikethis:

    funeditCustomer(customer:Customer){find(mapOf(CustomerEditor::customertocustomer).openWindow())}

    3.Components

    26

  • Theparametersarepassedasamap,wherethekeyisthepropertyintheviewandthevalueiswhateveryouwantthepropertytobe.ThisgivesyouatypesafewayofconfiguringparametersforthetargetView.

    HereweusetheKotlintosyntaxtocreatetheparameter.ThiscouldalsohavebeenwrittenasPair(CustomerEditor::customer,customer)ifyouprefer.Theeditorcannowaccesstheparameterlikethis:

    classCustomerEditor:Fragment(){valcustomer:Customerbyparam()

    }

    Ifyouwanttoinspecttheparametersinsteadofblindlyrelyingonthemtobeavailable,youcaneitherdeclarethemasnullableorconsulttheparamsmap:

    classCustomerEditor:Fragment(){init{valcustomer=params["customer"]as?Customerif(customer!=null){...}}}

    Ifyoudon'tcareabouttypesafetyyoucanalsopassparametersasmapOf("customer"tocustomer),butthenyoumissoutonautomaticrefactoringifyourenameapropertyinthetargetview.

    AccessingtheprimarystageViewhasapropertycalledprimaryStagethatallowsyoutomanipulatepropertiesoftheStagebackingit,suchaswindowsizeforexample.AnyVieworFragmentthatwereopenedviaopenModalwillalsohaveamodalStagepropertyavailable.

    AccessingthesceneSometimesitisnecessarytogetaholdofthecurrentscenefromwithinaVieworFragment.Thiscanbeachievedwithroot.scene,orifyouarewithinatypesafebuilder,thereisanevenshorterway,justusescene.

    3.Components

    27

  • AccessingresourcesLot'sofJavaFXAPIstakesresourcesasanURLorthetoExternalFormofanURL.Toretrievearesourceurlonewouldtypicallywritesomethinglike:

    valmyAudioClip=AudioClip(MyView::class.java.getResource("mysound.wav").toExternalForm())

    EveryComponenthasaresourcesobjectwhichcanretrievetheexternalformurlofaresourcelikethis:

    valmyAudiClip=AudioClip(resources["mysound.wav"])

    IfyouneedanactualURLitcanberetrievedlikethis:

    valmyResourceURL=resources.url("mysound.wav")

    TheresourceshelperalsohasseveralotherhelpfulfunctionstohelpyouturnfilesrelativetotheComponentintoanobjectofthetypeyouneed:

    valmyJsonObject=resources.json("myobject.json")valmyJsonArray=resources.jsonArray("myarray.json")valmyStream=resources.stream("somefile")

    It'sworthmentioningthatthejsonandjsonArrayfunctionsarealsoavailableonInputStreamobjects.

    ResourcesarerelativetotheComponentbutyoucanalsoretrievearesourcebyit'sfullpath,startingwitha/.

    ShortcutsandkeycombinationsforactionsYoucanfireactionswhencertainkeycombinationsaretyped.Thisisdonewiththeshortcutfunction:

    shortcut(KeyCombination.valueOf("Ctrl+Y")){doSomething()}

    3.Components

    28

  • Thereisalsoastringversionoftheshortcutfunctionthatdoesthesamebutislessverbose:

    shortcut("Ctrl+Y")){doSomething()}

    Youcanalsoaddshortcutstobuttonactionsdirectly:

    button("Save"){action{doSave()}shortcut("Ctrl+S")}

    TouchSupport

    JavaFXsupportstouchoutofthebox,andfornowtheonlyplaceweneededtoimproveitwastohandleshortpressandlongpressinamoreconvenientway.Itconsistsoftwofunctionssimilartoaction,whichcanbeconfiguredonanyNode:

    shortpress{println("Activatedonshortpress")}longpress{println("Activatedonlongpress")}

    Bothfunctionsacceptsaconsumeparameterwhichbydefaultisfalse.Settingittotruewillpreventeventbubblingforthepressevent.Thelongpressfunctionadditionallysupportsathresholdparameterwhichisusedtodeterminewhenalongpresshasaccured.Itis700.millisbydefault.

    SummaryTornadoFXisfilledwithsimple,streamlined,andpowerfulinjectiontoolstomanageViewsandControllers.ItalsostreamlinesdialogsandothersmallUIpiecesusingFragment.Whiletheapplicationswebuiltsofarareprettysimple,hopefullyyouappreciatethesimplifiedconceptsTornadoFXintroducestoJavaFX.InthenextchapterwewillcoverwhatisarguablythemostpowerfulfeatureofTornadoFX:Type-SafeBuilders.

    3.Components

    29

  • BasicControlsOneofthemostexcitingfeaturesofTornadoFXaretheType-SafeBuilders.ConfiguringandlayingoutcontrolsforcomplexUI'scanbeverboseanddifficult,andthecodecanquicklybecomemessytomaintain.Fortunately,youcanuseapowerfulclosurepatternpioneeredbyGroovytocreatestructuredUIlayoutswithpureandsimpleKotlincode.

    WhilewewilllearnhowtoapplyFXMLlater,youmayfindbuilderstobeanexpressive,robustwaytocreatecomplexUI'sinafractionofthetime.Therearenoconfigurationfilesorcompilermagictricks,andbuildersaredonewithpureKotlincode.Thenextseveralchapterswilldividethebuildersintoseparatecategoriesofcontrols.Alongtheway,youwillgraduallybuildmorecomplexUI'sbyintegratingthesebuilderstogether.

    Butfirst,let'scoverhowbuildersactuallywork.

    HowBuildersWorkKotlin'sstandardlibrarycomeswithahandfulofhelpful"block"functionstotargetitemsofanytypeT.Thereisthewith()function,whichallowsyoutowritecodeagainstanitemasifyouwererightinsideofitsclass.

    classMyView:View(){

    overridevalroot=VBox()

    init{with(root){this+=Button("PressMe")}}}

    Intheaboveexample,thewith()functionacceptstherootasanargument.Thefollowingclosureargumentmanipulatesrootdirectlybyreferringtoitasthis,whichissafelyinterpretedasaVBox.AButtonwasaddedtotheVBoxbycallingit'splusAssign()extendedoperator.

    Alternatively,everytypeinKotlinhasanapply()function.Thisisalmostthesamefunctionalityaswith()butitisactuallyanextendedhigher-orderfunction.

    4.BasicControls

    30

    https://kotlinlang.org/docs/reference/type-safe-builders.htmlhttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.htmlhttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html

  • classMyView:View(){

    overridevalroot=VBox()

    init{root.apply{this+=Button("PressMe")}}}

    Bothwith()andapply()accomplishasimilartask.Theysafelyinterpretthetypetheyaretargetingandallowmanipulationstobedonetoit.However,with()returnsthelaststatementwithinthelambda,whereasapply()doesinfactreturntheitemitwastargeting.Therefore,ifyoucallapply()onaButtontomanipulatesay,itsfontcolorandaction,itishelpfultheButtonreturnsitselfsoastonotbreakthedeclarationflow.

    classMyView:View(){

    overridevalroot=VBox()

    init{with(root){this+=Button("PressMe").apply{textFill=Color.REDaction{println("Buttonpressed!")}}}}}

    Thebasicconceptsofhowbuildersworkareexpressedabove,andtherearethreetasksbeingdone:

    1. AButtoniscreated2. TheButtonismodified3. TheButtonisaddedtoits"parent",whichisaVBox

    WhendeclaringanyNode,thesethreestepsaresocommonthatTornadoFXstreamlinesthemforyouusingstrategicallyplacedextensionfunctions,suchasbutton()asshownbelow.

    4.BasicControls

    31

  • classMyView:View(){

    overridevalroot=VBox()

    init{with(root){button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}}}}

    Whilethislooksmuchcleaner,youmightbewondering:"Howdidwejustgetridofthethis+=andapply()functioncall?Andwhyareweusingafunctioncalledbutton()insteadofanactualButton?"Wewillnotgotoodeeponhowthisisdone,andyoucanalwaysdigintothesourcecodeifyouarecurious.Butessentially,theVBox(oranytargetablecomponent)hasanextensionfunctioncalledbutton().ItacceptsatextargumentandanoptionalclosuretargetingaButtonitwillinstantiate.Whenthisfunctioniscalled,itwillcreateaButtonwiththespecifiedtext,applytheclosuretoit,addittotheVBoxitwascalledon,andthenreturnit.

    Takingthisefficiencyfurther,youcanoverridetherootinaView,butassignitabuilderfunctionandavoidneedinganyinitandwith()blocks.

    classMyView:View(){

    overridevalroot=vbox{button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}}}

    Thebuilderpatternbecomesespeciallypowerfulwhenyoustartnestingcontrolsintoothercontrols.Usingthesebuilderextensionfunctions,youcaneasilypopulateandnestmultipleHBoxinstancesintoaVBox,andcreateUIcodethatisclearlystructured(Figure4.1).

    4.BasicControls

    32

    https://github.com/edvin/tornadofx/blob/master/src/main/java/tornadofx/Controls.kt#L234

  • classMyView:View(){

    overridevalroot=vbox{hbox{label("FirstName")textfield()}hbox{label("LastName")textfield()}button("LOGIN"){useMaxWidth=true}}}

    Figure4.1

    AlsonotewewilllearnaboutTornadoFX'sproprietaryFormlater,whichwillmakesimpleinputUI'slikethisevensimplertocode.

    IfyouneedtosavereferencestocontrolssuchastheTextFields,youcansavethemtovariablesorpropertiessincethefunctionsreturntheproducedcontrols.ItisrecommendyouusethesingleAssign()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=trueaction{println("Logginginas${firstNameField.text}${lastNameField.text}")}}}

    }

    Notethatnon-builderextensionfunctionsandpropertieshavebeenaddedtodifferentcontrolsaswell.TheuseMaxWidthisanextendedpropertyforNode,anditsetstheNodetooccupythemaximumwidthallowed.Wewillseemoreofthesehelpfulextensionsthroughoutthenextfewchapters.

    Inthecomingchapters,wewillcovereachcorrespondingbuilderforeachJavaFXcontrol.Withtheconceptsunderstoodabove,youcanreadaboutthesenextchaptersstarttofinishorasareference.

    BuildersforBasicControlsTherestofthischapterwillcoverbuildersforcommonJavaFXcontrolslikeButton,Label,andTextField.Thenextchapterwillcoverbuildersfordata-drivencontrolslikeListView,TableView,andTreeTableView.

    Button

    ForanyPane,youcancallitsbutton()extensionfunctiontoaddaButtontoit.YoucanoptionallypassatextargumentandaButton.()->Unitlambdatomodifyitsproperties.

    4.BasicControls

    34

  • WithinaPane,thiswilladdaButtonwithredtextandprint"Buttonpressed!"everytimeitisclicked(Figure4.2)

    button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}

    Figure4.2

    Label

    Youcancallthelabel()extensionfunctiontoaddaLabeltoagivenPane.Optionallyyoucanprovideatext(oftypeStringorProperty),agraphic(oftypeNodeorObjectProperty)andaLabel.()->Unitlambdatomodifyitsproperties(Figure4.3).

    label("Loremipsum",circle(10,10,5)){textFill=Color.BLUE}

    Figure4.3

    TextField

    ForanyPaneyoucanaddaTextFieldbycallingitstextfield()extensionfunction(Figure4.4).

    textfield()

    Figure4.4

    4.BasicControls

    35

  • YoucanoptionallyprovideinitialtextaswellasaclosuretomanipulatetheTextField.Forexample,wecanaddalistenertoitstextProperty()andprintitsvalueeverytimeitchanges(Figure4.5).

    textfield("Inputsomething"){textProperty().addListener{obs,old,new->println("Youtyped:"+new)}}

    Figure4.6

    PasswordField

    IfyouneedaTextFieldtotakesensitiveinformation,youmightwanttoconsideraPasswordFieldinstead.Itwillshowanonymouscharacterstoprotectfrompryingeyes.Youcanalsoprovideaninitialpasswordasanargumentandablocktomanipulateit(Figure4.7).

    passwordfield("my_password"){requestFocus()}

    Figure4.7

    CheckBox

    YoucancreateaCheckBoxtoquicklycreateatrue/falsestatecontrolandoptionallymanipulateitwithablock(Figure4.8).

    4.BasicControls

    36

  • checkbox("AdminMode"){action{println(isSelected)}}

    Noticethattheactionblockiswrappedinsidethecheckboxsoyoucanaccessit'sisSelectedproperty.Ifyoudon'tneedaccesstothepropertiesoftheCheckBoxyoucouldhavewrittencheckbox("AdminMode").action{}.

    Figure4.9

    YoucanalsoprovideaPropertythatwillbindtoitsselectionstate.

    valbooleanProperty=SimpleBooleanProperty()

    checkbox("AdminMode",booleanProperty).action{println(isSelected)}

    ComboBox

    AComboBoxisadropdowncontrolthatallowsafixedsetofvaluestobeselectedfrom(Figure4.10).

    valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")

    combobox{items=texasCities}

    Figure4.10

    4.BasicControls

    37

  • Youdonotneedtospecifythegenerictypeifyoudeclarethevaluesasanargument.

    valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")

    combobox(values=texasCities)

    YoucanalsospecifyaPropertytobeboundtotheselectedvalue.

    valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")

    valselectedCity=SimpleStringProperty()

    combobox(selectedCity,texasCities)

    ToggleButton

    AToggleButtonisabuttonthatexpressesatrue/falsestatedependingonitsselectionstate(Figure4.11).

    togglebutton("OFF"){action{text=if(isSelected)"ON"else"OFF"}}

    4.BasicControls

    38

  • PerhapsamoreidomaticwaytocontrolthebuttontextwouldbetouseaStringBindingboundtothetextProperty:

    togglebutton{valstateText=selectedProperty().stringBinding{if(it==true)"ON"else"OFF"}textProperty().bind(stateText)}

    Figure4.11

    YoucanoptionallypassaToggleGrouptothetogglebutton()function.ThiswillensureallToggleButtonsinthatToggleGroupcanonlyhaveoneselectedatatime(Figure4.12).

    classMyView:View(){

    privatevaltoggleGroup=ToggleGroup()

    overridevalroot=hbox{togglebutton("YES",toggleGroup)togglebutton("NO",toggleGroup)togglebutton("MAYBE",toggleGroup)}}

    Figure4.12

    RadioButton

    ARadioButtonisthesamefunctionalityasaToggleButtonbutwithadifferentvisualstyle.Whenitisselected,it"fills"inacircularcontrol(Figure4.13).

    radiobutton("PowerUserMode"){action{println("PowerUserMode:$isSelected")}}

    4.BasicControls

    39

  • Figure4.13

    AlsoliketheToggleButton,youcansetaRadioButtontobeincludedinaToggleGroupsothatonlyoneiteminthatgroupcanbeselectedatatime(Figure4.14).

    classMyView:View(){

    privatevaltoggleGroup=ToggleGroup()

    overridevalroot=vbox{radiobutton("Employee",toggleGroup)radiobutton("Contractor",toggleGroup)radiobutton("Intern",toggleGroup)}}

    Figure4.14

    DatePicker

    TheDatePickerisasimpletodeclare.Itallowsyoutochooseadatefromapopoutcalendarcontrol.Youcanoptionallyprovideablocktomanipulateit(Figure4.15).

    datepicker{value=LocalDate.now()}

    Figure4.15

    4.BasicControls

    40

  • YoucanalsoprovideaPropertyasanargumenttobindtoitsvalue.

    valdateProperty=SimpleObjectProperty()

    datepicker(dateProperty){value=LocalDate.now()}

    TextArea

    TheTextAreaallowsyouinputmultilinefreeformtext.Youcanoptionallyprovidetheinitialtextvalueaswellasablocktomanipulateitondeclaration(Figure4.16).

    textarea("Typememohere"){selectAll()}

    Figure4.16

    4.BasicControls

    41

  • ProgressBar

    AProgressBarvisualizesprogresstowardscompletionofaprocess.YoucanoptionallyprovideaninitialDoublevaluelessthanorequalto1.0indicatingpercentageofcompletion(Figure4.17).

    progressbar(0.5)

    Figure4.17

    Hereisamoredynamicexamplesimulatingprogressoverashortperiodoftime.

    progressbar(){thread{for(iin1..100){Platform.runLater{progress=i.toDouble()/100.0}Thread.sleep(100)}}}

    YoucanalsopassaPropertythatwillbindtheprogresstoitsvalueaswellasablocktomanipulatetheProgressBar.

    4.BasicControls

    42

  • progressbar(completion){progressProperty().addListener{obsVal,old,new->print("VALUE:$new")}}

    ProgressIndicator

    AProgressIndicatorisfunctionallyidenticaltoaProgressBarbutusesafillingcircleinsteadofabar(Figure4.18).

    progressindicator{thread{for(iin1..100){Platform.runLater{progress=i.toDouble()/100.0}Thread.sleep(100)}}}

    Figure4.18

    JustliketheProgressBaryoucanprovideaPropertyand/orablockasoptionalarguments(Figure4.19).

    valcompletion=SimpleObjectProperty(0.0)progressindicator(completion)

    ImageView

    Youcanembedanimageusingimageview().

    imageview("tornado.jpg")

    Figure4.19

    4.BasicControls

    43

  • Likemostothercontrols,youcanuseablocktomodifyitsattributes(Figure4.20).

    imageview("tornado.jpg"){scaleX=.50scaleY=.50}

    Figure4.20

    4.BasicControls

    44

  • ScrollPane

    YoucanembedacontrolinsideaScrollPanetomakeitscrollable.Whentheavailableareabecomessmallerthanthecontrol,scrollbarswillappeartonavigatethecontrol'sarea.

    Forinstance,youcanwrapanImageViewinsideaScrollPane(Figure4.21).

    scrollpane{imageview("tornado.jpg")}

    Figure4.21

    4.BasicControls

    45

  • KeepinmindthatmanycontrolslikeTableViewandTreeTableViewalreadyhavescrollbarsonthem,sowrappingtheminaScrollPaneisnotnecessary(Figure4.22).

    Hyperlink

    YoucancreateaHyperlinkcontroltomimicthebehaviorofatypicalhyperlinktoafile,awebsite,orsimplyperformanaction.

    hyperlink("OpenFile").action{println("Openingfile...")}

    Figure4.22

    Text

    YoucanaddasimplepieceofTextwithformattedproperties.ThiscontrolissimplerandrawerthanaLabel,andparagraphscanbeseparatedusing\ncharacters(Figure4.23).

    4.BasicControls

    46

  • text("Veni\nVidi\nVici"){fill=Color.PURPLEfont=Font(20.0)}

    Figure4.23

    TextFlow

    Ifyouneedtoconcatenatemultiplepiecesoftextwithdifferentformats,theTextFlowcontrolcanbehelpful(Figure4.24).

    textflow{text("Tornado"){fill=Color.PURPLEfont=Font(20.0)}text("FX"){fill=Color.ORANGEfont=Font(28.0)}}

    Figure4.24

    YoucanaddanyNodetothetextflow,includingimages,usingthestandardbuilderfunctions.

    Tooltips

    InsideanyNodeyoucanspecifyaTooltipviathetooltip()function(Figure4.25).

    4.BasicControls

    47

  • button("Commit"){tooltip("Writesinputtothedatabase")}

    Figure4.25

    Likemostotherbuilders,youcanprovideaclosuretocustomizetheTooltipitself.

    button("Commit"){tooltip("Writesinputtothedatabase"){font=Font.font("Verdana")}}

    Therearemanyotherbuildercontrols,andthemaintainersofTornadoFXhavestrivedtocreateabuilderforeveryJavaFXcontrol.Ifyouneedsomethingthatisnotcoveredhere,useGoogletoseeifitsincludedinJavaFX.ChancesareifacontrolisavailableinJavaFX,thereisabuilderwiththesamenameinTornadoFX.

    SUMMARY

    InthischapterwelearnedaboutTornadoFXbuildersandhowtheyworksimplybyusingKotlinextensionfunctions.WealsocoveredbuildersforbasiccontrolslikeButton,TextFieldandImageView.Inthecomingchapterswewilllearnaboutbuildersfortables,layouts,menus,charts,andothercontrols.Asyouwillsee,combiningallthesebuilderstogethercreatesapowerfulwaytoexpresscomplexUI'swithverystructuredandminimalcode.

    ThesearenottheonlycontrolbuildersintheTornadoFXAPI,andthisguidedoesitsbesttokeepup.AlwayschecktheTornadoFXGitHubtoseethelatestbuildersandfunctionalitiesavailable,andfileanissueifyouseeanymissing.

    4.BasicControls

    48

    https://github.com/edvin/tornadofx

  • 4.BasicControls

    49

  • DataControlsAnysignificantapplicationworkswithdata,andprovidingameansforuserstoview,manipulate,andmodifydataisnotatrivialtaskforuserinterfacedevelopment.Fortunately,TornadoFXstreamlinesmanyJavaFXdatacontrolssuchasListView,TableView,TreeView,andTreeTableView.Thesecontrolscanbecumbersometosetupinapurelyobject-orientedway.Butusingbuildersthroughfunctionaldeclarations,wecancodeallthesecontrolsinamuchmorestreamlinedway.

    ListView

    AListViewissimilartoaComboBoxbutitdisplaysallitemswithinaScrollViewandhastheoptionofallowingmultipleselections,asshowninFigure5.1

    listview{items.add("Alpha")items.add("Beta")items.add("Gamma")items.add("Delta")items.add("Epsilon")selectionModel.selectionMode=SelectionMode.MULTIPLE}

    Figure5.1

    YoucanalsoprovideitanObservableListofitemsupfrontandomitthetypedeclarationsinceitcanbeinferred.UsinganObservableListalsohasthebenefitthatchangestothelistwillautomaticallybereflectedintheListView.

    5.DataControls

    50

  • valgreekLetters=listOf("Alpha","Beta","Gamma","Delta","Epsilon").observable()

    listview(greekLetters){selectionModel.selectionMode=SelectionMode.MULTIPLE}

    Likemostdatacontrols,keepinmindthatbydefault,theListViewwillcalltoString()torenderthetextforeachiteminyourdomainclass.Torenderanythingelse,youwillneedtocreateyourowncustomcellformatting.

    CustomCellFormattinginListView

    EventhoughthedefaultlookofaListViewisratherboring(becauseitcallstoString()andrendersitastext)youcanmodifyitsothateverycellisacustomNodeofyourchoosing.BycallingcellCache(),TornadoFXprovidesaconvenientwaytooverridewhatkindofNodeisreturnedforeachiteminyourlist(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_RIGHTstyle{fontSize=22.pxfontWeight=FontWeight.BOLD}}}}}}}}

    classPerson(valname:String,valbirthday:LocalDate){valage:Intget()=Period.between(birthday,LocalDate.now()).years}

    5.DataControls

    52

  • Figure5.2-AcustomcellrenderingforListView

    ThecellFormatfunctionletsyouconfigurethetextand/orgraphicpropertyofthecellwheneveritcomesintoviewonthescreen.Thecellsthemselvesarereused,butwhenevertheListViewasksthecelltoupdateit'scontent,thecellFormatfunctioniscalled.Inourexampleweonlyassigntographic,butifyoujustwanttochangethestringrepresentationyoushouldassignittotext.Itiscompletelylegitimatetoassignittobothtextandgraphic.ThevalueswillautomaticallybeclearedbythecellFormatfunctionwhenacertainlistcellisnotshowinganactiveitem.

    Notethatassigningnewnodestothegraphicpropertyeverytimethelistcellisaskedtoupdatecanbeexpensive.Itmightbefineformanyusecases,butforheavynodegraphs,ornodegraphswhereyouutilizebindingtowardstheuicomponentsinsidethecell,youshouldcachetheresultingnodesothenodegraphwillonlybecreatedoncepernode.Thisisdoneusingthecachewrapperintheaboveexample.

    5.DataControls

    53

  • AssignIfNull

    Ifyouhaveareasonforwantingtorecreatethegraphicpropertyforalistcell,youcanusetheassignIfNullhelper,whichwillassignavaluetoanygivenpropertyifthepropertydoesn'talreadycontainavalue.ThiswillmakesurethatyouavoidcreatingnewnodesifupdateItemiscalledonacellthatalreadyhasagraphicpropertyassigned.

    cellFormat{graphicProperty().assignIfNull{label("Hello")}}

    ListCellFragment

    TheListCellFragmentisaspecialfragmentwhichcanhelpyoumanageListViewcells.ItextendsFragment,andincludessomeextraListViewspecificfieldsandhelpers.Youneverinstantiatethesefragmentsmanually,insteadyouinstructtheListViewtocreatethemasneeded.ThereisaonetoonecorrelationbetweenListCellandListCellFragmentinstances.OneListCellFragmentinstancewilloveritslifecyclebeusedtorepresentdifferentitems.

    Tounderstandhowthisworks,let'sconsideramanuallyimplementedListCell,essentiallythewayyouwoulddoinvanillaJavaFX.TheupdateItemfunctionwillbecalledwhentheListCellshouldrepresentanewitem,noitem,orjustanupdatetothesameitem.WhenyouuseaListCellFragment,youdonotneedtoimplementsomethingakintoupdateItem,buttheitemPropertyinsideitwillupdatetorepresentthenewitemautomatically.YoucanlistentochangestotheitemProperty,orbetteryet,binditdirectlytoaViewModel.ThatwayyourUIcanbinddirectlytotheViewModelandnolongerneedtocareaboutchangestotheunderlyingitem.

    Let'srecreatetheformfromthecellFormatexampleusingaListCellFragment.WeneedaViewModelwhichwewillcallPersonModel(PleaseseetheEditingModelsandValidationchapterforafullexplanationoftheViewModel)Fornow,justimaginethattheViewModelactsasaproxyforanunderlyingPerson,andthatthePersoncanbechangedwhiletheobservablevaluesintheViewModelremainthesame.WhenwehavecreatedourPersonCellFragment,weneedtoconfiguretheListViewtouseit:

    listview(personlist){cellFragment(PersonCellFragment::class)}

    5.DataControls

    54

  • NowcomestheListCellFragmentitself.

    classPersonListFragment:ListCellFragment(){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_RIGHTstyle{fontSize=22.pxfontWeight=FontWeight.BOLD}}}}}

    BecausethisFragmentwillbereusedtorepresentdifferentlistitems,theeasiestapproachistobindtheuielementstotheViewModel'sproperties.

    Thenameandbirthdaypropertiesarebounddirectlytothelabelsinsidethefields.TheagestringinthelastlabelneedstobeconstructedusingastringBindingtomakesureitupdateswhentheitemchanges.

    WhilethismightseemlikeslightlymoreworkthanthecellFormatexample,thisapproachmakesitpossibletoleverageeverythingtheFragmentclasshastooffer.Italsoforcesyoutodefinethecellnodegraphoutsideofthebuilderhierarchy,whichimprovesrefactoringpossibilitiesandenablescodereuse.

    Additionalhelpersandeditingsupport

    TheListCellFragmentalsohavesomeotherhelperproperties.TheyincludethecellPropertywhichwillupdatewhenevertheunderlyingcellchangesandtheeditingProperty,whichwilltellyouifthistheunderlyinglistcellisineditingmode.TherearealsoeditinghelperfunctionscalledstartEdit,commitEdit,cancelEditplusanonEditcallback.TheListCellFragmentmakesittrivialtoutilizetheexistingeditingcapabilitesoftheListView.AcompleteexamplecanbeseenintheTodoMVCdemoapplication.

    5.DataControls

    55

    https://github.com/edvin/todomvc

  • TableView

    ProbablyoneofthemostsignificantbuildersinTornadoFXistheoneforTableView.IfyouhaveworkedwithJavaFX,youmighthaveexperiencedbuildingaTableViewinanobject-orientedway.ButTornadoFXprovidesafunctionaldeclarationconstructpatternusingextensionfunctionsthatgreatlysimplifiesthecodingofaTableView.

    Sayyouhaveadomaintype,suchasPerson.

    classPerson(valid:Int,valname:String,valbirthday:LocalDate){valage:Intget()=Period.between(birthday,LocalDate.now()).years}

    TakeseveralinstancesofPersonandputtheminanObservableList.

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

    YoucanquicklydeclareaTableViewwithallofitscolumnsusingafunctionalconstruct,andspecifytheitemspropertytoanObservableList(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

  • Thecolumn()functionsareextensionfunctionsforTableViewacceptingaheadernameandamappedpropertyusingreflectionsyntax.TornadoFXwillthentakeeachmappingtorenderavalueforeachcellinthatgivencolumn.

    IfyouwantgranularcontroloverTableViewcolumnresizepolicies,seeAppendixA2formoreinformationonSmartResizepolicies.

    Using"Property"properties

    IfyoufollowtheJavaFXPropertyconventionstosetupyourdomainclass,itwillautomaticallysupportvalueediting.

    YoucancreatethesePropertyobjectstheconventionalway,oryoucanuseTornadoFX'spropertydelegatestoautomaticallycreatethesePropertydeclarationsasshownbelow.

    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}

    YouneedtocreatexxxProperty()functionsforeachpropertytosupportJavaFX'snamingconventionwhenitusesreflection.ThiscaneasilybedonebyrelayingtheircallstogetProperty()toretrievethePropertyforagivenfield.SeeAppendixA1fordetailed

    5.DataControls

    57

  • informationonhowthesepropertydelegateswork.

    NowontheTableView,youcanmakeiteditable,maptotheproperties,andapplytheappropriatecell-editingfactoriestomakethevalueseditable.

    overridevalroot=tableview(persons){isEditable=truecolumn("ID",Person::idProperty).useTextField(IntegerStringConverter())column("Name",Person::nameProperty).useTextField(DefaultStringConverter())column("Birthday",Person::birthdayProperty).useTextField(LocalDateStringConverter())column("Age",Person::age)}

    Toalloweditingandrendering,TornadoFXprovidesafewdefaultcellfactoriesyoucaninvokeonacolumneasilythroughextensionfunctions.

    ExtensionFunction Description

    useTextField() UsesastandardTextFieldtoeditvalueswithaprovidedStringConverter

    useComboBox() EditsacellvalueviaaComboBoxwithaspecifiedObservableListofapplicablevalues

    useChoiceBox() AcceptsvaluechangestoacellwithaChoiceBox

    useCheckBox() RendersaneditableCheckBoxforaBooleanvaluecolumn

    useProgressBar() RendersthecellasaProgressBarforaDoublevaluecolumn

    PropertySyntaxAlternatives

    IfyoudonotcareaboutexposingthePropertyinafunction(whichiscommoninpractialusage)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

  • ThisalternativepatternexposesthePropertyasafieldmemberinsteadofafunction.Ifyouliketheabovesyntaxbutwanttokeepthefunction,youcanmakethepropertyprivateandaddthefunctionlikethis:

    privatevalnameProperty=SimpleStringProperty(name)funnameProperty()=namePropertyvarnamebynameProperty

    Choosingfromthesepatternsareallamatteroftaste,andyoucanusewhateverversionmeetsyourneedsorpreferencesbest.

    YoucanalsoconvertplainpropertiestoJavaFXpropertiesusingtheTornadoFXPlugin.RefertoChapter13tolearnhowtodothis.

    UsingcellFormat()

    ThereareotherextensionfunctionsappliedtoTableViewthatcanassisttheflowofdeclaringaTableView.Forinstance,youcancallacellFormat()functiononagivencolumntoapplyformattingrules,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.WHITEtextFill=Color.BLACK}}}}

    Figure5.4

    5.DataControls

    59

  • Accessingnestedproperties

    Let'sassumeourPersonobjecthasaparentpropertywhichisalsoofoftypePerson.Tocreateacolumnfortheparentname,wehaveseveraloptions.Ourfirstattemptissimplyextractingthenamepropertymanually:

    column("Parentname",{it.value.parentProperty.value.nameProperty})

    Noticehowwecan'tsimplyreferencetheproperty,weneedtoaccessthevalueprovidedinthecallbacktogettotheactualinstanceandnestfromtheredowntothenameProperty.Whilethisworks,ithasonemajordrawback.Iftheparentchanges,thelistwon'tbeupdated.Wecanpartiallyremedythisbydefiningthevalueforthepropertyastheparentitself,andformattingit'sname:

    column("Parentname",Person::parentProperty).cellFormat{textProperty().bind(it.parentProperty.value.nameProperty)}

    Itmightstillnotupdaterightaway,eventhoughitwouldeventuallybecomeconsistentastheTableViewrefreshes.

    Tocreateabindingthatwouldreflectachangetotheparentpropertyimmediately,considerusingaselectbinding:(moreonbindingslater)

    column("Parentname",{it.value.parentProperty.select(Person::nameProperty)})

    5.DataControls

    60

  • DeclaringColumnValuesFunctionally

    Ifyouneedtomapacolumn'svaluetoanon-property(suchasafunction),youcanuseanon-reflectionmeanstoextractthevaluesforthatcolumn.

    SayyouhaveaWeeklyReporttypethathasagetTotal()functionacceptingaDayOfWeekargument(anenumofMonday,Tuesday...Sunday).

    abstractclassWeeklyReport(valstartDate:LocalDate){abstractfungetTotal(dayOfWeek:DayOfWeek):BigDecimal}

    Let'ssayyouwantedtocreateacolumnforeachDayOfWeek.Youcannotmaptoproperties,butyoucanmapeachWeeklyReportitemexplicitlytoextracteachvalueforthatDayOfWeek.

    tableview{for(dayOfWeekinDayOfWeek.values()){column(dayOfWeek.toString()){ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))}}}

    ThismorecloselyresemblesthetraditionalsetCellValueFactory()fortheJavaFXTableColumn.

    RowExpanders

    LaterwewilllearnabouttheTreeTableViewwhichhasanotionof"parent"and"child"rows,buttheconstraintwiththiscontrolistheparentandchildmusthavethesamecolumns.Fortunately,TornadoFXcomeswithanawesomeutilitytonotonlyreveala"childtable"foragivenrow,butanykindofNodecontrol.

    Saywehavetwodomaintypes:RegionandBranch.ARegionisageographicalzone,anditcontainsoneormoreBranchitemswhicharespecificbusinessoperationlocations(warehouses,distributioncenters,etc).Hereisadeclarationofthesetypesandsomegiveninstances.

    5.DataControls

    61

  • classRegion(valid:Int,valname:String,valcountry:String,valbranches:ObservableList)

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

    WecancreateaTableViewwhereeachrowhasarowExpander()functiondefined,andtherewecanarbitrarilycreateanyNodecontrolbuiltoffthatparticularrow'sitem.Inthiscase,wecannestanotherTableViewforagivenRegiontoshowalltheBranchitemsbelongingtoit.Itwillhavea"+"buttoncolumntoexpandandshowthisexpandedcontrol(Figure5.5).

    Figure5.5

    5.DataControls

    62

  • Thereareafewconfigurabilityoptions,like"expandondouble-click"behaviorsandaccessingtheexpanderColumn(thecolumnwiththe"+"button)todriveapadding(Figure5.6).

    overridevalroot=tableview(regions){column("ID",Region::id)column("Name",Region::name)column("Country",Region::country)rowExpander(expandOnDoubleClick=true){paddingLeft=expanderColumn.widthtableview(it.branches){column("ID",Branch::id)column("FacilityCode",Branch::facilityCode)column("City",Branch::city)column("State/Province",Branch::stateProvince)}}}

    Figure5.6

    5.DataControls

    63

  • TherowExpander()functiondoesnothavetoreturnaTableViewbutanykindofNode,includingFormsandothersimpleorcomplexcontrols.

    Accessingtheexpandercolumn

    Youmightwanttomanipulateorcallfunctionsontheactualexpandercolumn.Ifyouactivateexpandondoubleclick,youmightnotwanttoshowtheexpandercolumninthetableatall.Firstweneedareferencetotheexpander:

    valexpander=rowExpander(true){...}

    Ifyouwanttohidetheexpandercolumn,justcallexpander.isVisible=false.Youcanalsoprogrammaticallytoggletheexpandedstateofanycolumnbycallingexpander.toggleExpanded(rowIndex).

    TreeView

    TheTreeViewcontainselementswhereeachelementmaycontainchildelements.Typicallyarrowsallowyoutoexpandaparentelementtoseeitschildren.Forinstance,wecannestemployeesunderdepartmentnames

    5.DataControls

    64

  • TraditionallyinJavaFX,populatingtheseelementsisrathercumbersomeandverbose.FortunatelyTornadoFXmakesitrelativelysimple.

    SayyouhaveasimpletypePersonandanObservableListcontainingseveralinstances.

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

    CreatingaTreeViewwiththetreeview()buildercanbedonefunctionallyFigure5.7).

    //CreatePersonobjectsforthedepartments//withthedepartmentnameasPerson.name

    valdepartments=persons.map{it.department}.distinct().map{Person(it,"")}

    treeview{//Createrootitemroot=TreeItem(Person("Departments",""))

    //MakesurethetextineachTreeItemisthenameofthePersoncellFormat{text=it.name}

    //Generateitems.Childrenoftherootitemwillcontaindepartmentspopulate{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,"")}

    Firstwegatheradistinctlistofallthedepartmentsderivedfromthepersonslist.ButthenweputeachdepartmentStringinaPersonobjectsincetheTreeViewonlyacceptsPersonelements.Whilethisisnotveryintuitive,thisistheconstraintanddesignofTreeView.WemustmakeeachdepartmentaPersonforittobeaccepted.

    treeview{//Createrootitemroot=TreeItem(Person("Departments",""))

    5.DataControls

    66

  • NextwespecifythehighestrootfortheTreeViewthatalldepartmentswillbenestedunder,andwegiveitaplaceholderPersoncalled"Departments".

    cellFormat{text=it.name}

    ThenwespecifythecellFormat()torenderthenameofeachPerson(includingdepartments)oneachcell.

    populate{parent->if(parent==root)departmentselsepersons.filter{it.department==parent.value.name}}

    Finally,wecallthepopulate()functionandprovideablockinstructinghowtoprovidechildrentoeachparent.Iftheparentisindeedtheroot,thenwereturnthedepartments.OtherwisetheparentisadepartmentandweprovidealistofPersonobjectsbelongingtothatdepartment.

    DatadrivenTreeView

    IfthechildlistyoureturnfrompopulateisanObservableList,anychangestothatlistwillautomaticallybereflectedintheTreeView.Thepopulatefunctionwillbecalledforanynewchildrenthatappears,andremoveditemswillresultinremovedTreeItemsaswell.

    TreeViewwithDifferingTypes

    ItisnotnecessarilyintuitivetomakeeveryentityinthepreviousexampleaPerson.WemadeeachdepartmentaPersonaswellastheroot"Departments".ForamorecomplexTreeViewwhereTisunknownandcanbeanynumberoftypes,itisbettertoleveragestarprojectionfortypeT.

    Usingstarprojection,youcansafelypopulatemultipletypesnestedintotheTreeView.

    Forinstance,youcancreateaDepartmenttypeandleveragecellFormat()toutilizetype-checkingforrendering.Thenyoucanuseapopulate()functionthatwilliterateovereachelement,andyouspecifythechildrenforeachelement(ifany).

    5.DataControls

    67

    http://kotlinlang.org/docs/reference/generics.html#star-projections

  • dataclassDepartment(valname:String)

    //CreateDepartmentobjectsforthedepartmentsbygettingdistinctvaluesfromPerson.departmentvaldepartments=persons.map{it.department}.distinct().map{Department(it)}

    //TypesafewayofextractingthecorrectTreeItemtextcellFormat{text=when(it){isString->itisDepartment->it.nameisPerson->it.nameelse->throwIllegalArgumentException("Invalidvaluetype")}}

    //Generateitems.Childrenoftherootitemwillcontaindepartments,childrenofdepartmentsarefilteredpopulate{parent->valvalue=parent.valueif(parent==root)departmentselseif(valueisDepartment)persons.filter{it.department==value.name}elsenull}

    TreeTableView

    TheTreeTableViewoperatesandfunctionssimilarlytoaTreeView,butithasmultiplecolumnssinceitisatable.PleasenotethatthecolumnsinaTreeTableViewarethesameforeachparentandchildelement.Ifyouwantthecolumnstobedifferentbetweenparentandchild,useaTableViewwitharowExpander()ascoveredearlierinthischapter.

    SayyouhaveaPersonclassthatoptionallyhasanemployeesparameter,whichdefaultstoanemptyListifnobodyreportstothatPerson.

    classPerson(valname:String,valdepartment:String,valemail:String,valemployees:List=emptyList())

    ThenyouhaveanObservableListholdinginstancesofthisclass.

    5.DataControls

    68

  • valpersons=listOf(Person("MaryHanes","ITAdministration","[email protected]",listOf(Person("JacobMays","ITHelpDesk","[email protected]"),Person("JohnRamsy","ITHelpDesk","[email protected]"))),Person("ErinJames","HumanResources","[email protected]",listOf(Person("ErlickFoyes","CustomerService","[email protected]"),Person("SteveFolley","CustomerService","[email protected]"),Person("LarryCable","CustomerService","[email protected]")))).observable()

    YoucancreateaTreeTableViewbymergingthecomponentsneededforaTableViewandTreeViewtogether.Youwillneedtocallthepopulate()functionaswellassettherootTreeItem.

    valtreeTableView=TreeTableView().apply{column("Name",Person::nameProperty)column("Department",Person::departmentProperty)column("Email",Person::emailProperty)

    ///Createtherootitemthatholdsalltoplevelemployeesroot=TreeItem(Person("Employeesbyleader","","",persons))

    //Alwaysreturnemployeesunderthecurrentpersonpopulate{it.value.employees}

    //Expandthetwofirstlevelsroot.isExpanded=trueroot.children.forEach{it.isExpanded=true}

    //ResizetodisplayallelementsonthefirsttwolevelsresizeColumnsToFitContent()}

    ItisalsopossibletoworkwithmoreofanadhocbackingstorelikeaMap.Thatwouldlooksomethinglikethis:

    5.DataControls

    69

  • valtableData=mapOf("Fruit"toarrayOf("apple","pear","Banana"),"Veggies"toarrayOf("beans","cauliflower","cale"),"Meat"toarrayOf("poultry","pork","beef"))

    treetableview(TreeItem("Items")){column("Type",{it.value.valueProperty()})populate{if(it.value=="Items")tableData.keyselsetableData[it.value]?.asList()}}

    DataGrid

    ADataGridissimilartotheGridPaneinthatitdisplaysitemsinaflexiblegridofrowsandcolumns,butthesimilaritiesendsthere.WhiletheGridPanerequiresyoutoaddNodestothechildrenlist,theDataGridisdatadriveninthesamewayasTableViewandListView.Yousupplyitwithalistofitemsandtellithowtoconvertthosechildrentoagraphicalrepresentation.

    Itsupportsselectionofeitherasingleitemormultipleitemsatatimesoitcanbeusedasforexamplethedisplayofanimageviewerorothercomponentswhereyouwantavisualrepresentationoftheunderlyingdata.UsagewiseitisclosetoaListView,butyoucancreateanarbitraryscenegraphinsideeachcellsoitiseasytovisualizemultiplepropertiesforeachitem.

    valkittens=listOf("http://i.imgur.com/DuFZ6PQb.jpg","http://i.imgur.com/o2QoeNnb.jpg")//moreitemshere

    datagrid(kittens){cellCache{imageview(it)}}

    Figure5.8

    5.DataControls

    70

  • ThecellCachefunctionreceiveseachiteminthelist,andsinceweusedalistofStringsinourexample,wesimplypassthatstringtotheimageview()buildertocreateanImageViewinsideeachtablecell.ItisimportanttocallthecellCachefunctioninsteadofthecellFormatfunctiontoavoidrecreatingtheimageseverytimetheDataGridredraws.Itwillreusetheitems.

    Let'screateascenegraphthatisalittlebitmoreinvolved,andalsochangethedefaultsizeofeachcell:

    5.DataControls

    71

  • valnumbers=(1..10).toList()

    datagrid(numbers){cellHeight=75.0cellWidth=75.0

    multiSelect=true

    cellCache{stackpane{circle(radius=25.0){fill=Color.FORESTGREEN}label(it.toString())}}}

    Figure5.9

    Thegridissuppliedwithalistofnumbersthistime.Westartbyspecifyingacellheightandwidthof75pixels,halfofthedefaultsize.Wealsoconfiguremultiselecttobeabletoselectmorethanasingleelement.ThisisashortcutofwritingselectionModel.selectionMode=SelectionMode.MULTIPLEviaanextensionproperty.WecreateaStackPanethatstacksaLabelontopofaCircle.

    5.DataControls

    72

  • Youmightwonderwhythelabelgotsobigandboldbydefault.Thisiscomingfromthedefaultstylesheet.Thestylesheetisagoodstartingpointforfurthercustomization.AllpropertiesofthedatagridcanbeconfiguredincodeaswellasinCSS,andthestylesheetlistsallpossiblestyleproperties.

    Thenumberlistshowcasedmultipleselection.Whenacellisselected,itreceivestheCSSpseudoclassofselected.BydefaultitwillbehavemostlylikeaListViewrowwithregardstoselectionstyles.YoucanaccesstheselectionModelofthedatagridtolistenforselectionchanges,seewhatitemsareselectedetc.

    Summary

    FunctionalconstructsworkwellwithdatacontrolslikeTableView,TreeView,andotherswehaveseeninthischapter.Usingthebuilderpatterns,youcanquicklyandfunctionallydeclarehowdataisdisplayed.

    InChapter7,wewillembedcontrolsinlayoutstocreatemorecomplexUI'seasily.

    5.DataControls

    73

    https://github.com/edvin/tornadofx/blob/master/src/main/resources/tornadofx/datagrid.css

  • Type-SafeCSSWhileyoucancreateplaintextCSSstylesheetsinJavaFX,TornadoFXprovidestheoptiontobringtype-safetyandcompiledCSStoJavaFX.Youcanconvenientlychoosetocreatestylesinitsownclass,ordoitinlinewithinacontroldeclaration.

    InlineCSS

    ThequickestandeasiestwaytostyleacontrolontheflyistocallagivenNode'sinlinestyle{}function.AlltheCSSpropertiesavailableonagivencontrolareavailableinatype-safemanner,withcompilationchecksandauto-completion.

    Forexample,youcanstylethebordersonaButton(usingthebox()function),bolditsfont,androtateit(Figure6.1).

    button("PressMe"){style{fontWeight=FontWeight.EXTRA_BOLDborderColor+=box(top=Color.RED,right=Color.DARKGREEN,left=Color.ORANGE,bottom=Color.PURPLE)rotate=45.deg}

    setOnAction{println("Youpressedthebutton")}}

    Figure6.1

    ThisisespeciallyhelpfulwhenyouwanttostyleacontrolwithoutbreakingthedeclarationflowoftheButton.However,keepinmindthestyle{}willreplaceallstylesappliedtothatcontrolunlessyoupasstrueforitsoptionalappendargument.

    6.TypeSafeCSS

    74

  • style(append=true){....}

    Sometimesyouwanttoapplythesamestylestomanynodesinonego.Thestyle{}functioncanalsobeappliedtoanyIterablethatcontainsNodes:

    vbox{label("First")label("Second")label("Third")children.style{fontWeight=FontWeight.BOLD}}

    ThefontWeightstyleisappliedtoallchildrenofthevbox,inessenceallthelabelsweadded.

    Whenyourstylingcomplexitypassesacertainthreshold,youmaywanttoconsiderusingStylesheetswhichwewillcovernext.

    ApplyingStyleClasseswithStylesheets

    Ifyouwanttoorganize,re-use,combine,andoverridestylesyouneedtoleverageaStylesheet.TraditionallyinJavaFX,astylesheetisdefinedinaplainCSStextfileincludedintheproject.However,TornadoFXallowscreatingstylesheetswithpureKotlincode.Thishasthebenefitsofcompilationchecks,auto-completion,andotherperksthatcomewithstaticallytypedcode.

    TodeclareaStylesheet,extenditontoyourownclasstoholdyourcustomizedstyles.

    importtornadofx.*

    classMyStyle:Stylesheet(){}

    Next,youwillwanttospecifyitscompanionobjecttoholdclass-levelpropertiesthatcaneasilyberetrieved.Declareanewcssclass()-delegatedpropertycalledtackyButton,anddefinefourcolorswewilluseforitsborders.

    6.TypeSafeCSS

    75

  • importjavafx.scene.paint.Colorimporttornadofx.*

    classMyStyle:Stylesheet(){

    companionobject{valtackyButtonbycssclass()

    privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}}

    Notealsoyoucanusethec()functiontobuildcolorsquicklyusingRGBvaluesorcolorStrings.

    privatevaltopColor=c("#FF0000")privatevalrightColor=c("#006400")privatevalleftColor=c("#FFA500")privatevalbottomColor=c("#800080")

    Finally,declareaninit()blocktoapplystylingtotheclasses.Defineyourselectionandprovideablockthatmanipulatesitsvariousproperties.(Forcompoundselections,callthes()function,whichisanaliasfortheselect()function).Setrotateto10degrees,definetheborderColorusingthefourcolorsandthebox()function,makethefontfamily"ComicSansMS",andincreasethefontSizeto20pixels.NotethatthereareextensionpropertiesforNumbertypestoquicklyyieldthevalueinthatunit,suchas10.degfor10degreesand20.pxfor20pixels.

    6.TypeSafeCSS

    76

  • importjavafx.scene.paint.Colorimporttornadofx.*

    classMyStyle:Stylesheet(){

    companionobject{valtackyButtonbycssclass()

    privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}

    init{tackyButton{rotate=10.degborderColor+=box(topColor,rightColor,bottomColor,leftColor)fontFamily="ComicSansMS"fontSize=20.px}}}

    NowyoucanapplythetackyButtonstyletobuttons,labels,andothercontrolsthatsupporttheseproperties.Whilethisstylingcanworkwithothercontrolslikelabels,wearegoingtotargetbuttonsinthisexample.

    First,loadtheMyStylestylesheetintoyourapplicationbyincludingitascontructorparameter.

    classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}

    ThereloadStylesheetsOnFocus()functioncallwillinstructTornadoFXtoreloadtheStylesheetseverytimetheStagegetsfocus.Youcanalsopassthe--live-stylesheetsargumenttotheapplicationtoaccomplishthis.

    Important:Forthereloadtowork,youmustberunningtheJVMindebugmodeandyoumustinstructyourIDEtorecompilebeforeyouswitchbacktoyourapp.Withoutthesesteps,nothingwillhappen.ThisalsoappliestoreloadViewsOnFocus()whichissimilar,butreloadsthewholeviewinsteadofjustthestylesheet.Thisway,youcanevolveyourUIveryrapidlyina"codechange,compile,refresh"manner.

    6.TypeSafeCSS

    77

  • YoucanapplystylesdirectlytoacontrolbycallingitsaddClass()function.ProvidetheMyStyle.tackyButtonstyletotwobuttons(Figure6.2).

    classMyView:View(){overridevalroot=vbox{button("PressMe"){addClass(MyStyle.tackyButton)}button("PressMeToo"){addClass(MyStyle.tackyButton)}}}

    Figure6.2

    IntellijIDEAcanperformaquickfixtoimportmembervariables,allowingaddClass(MyStyle.tackyButton)tobeshortenedtoaddClass(tackyButton)ifyouprefer.

    YoucanuseremoveClass()toremovethespecifiedstyleaswell.

    TargetingStylestoaType

    OneofthebenefitsofusingpureKotlinisyoucantightlymanipulateUIcontrolbehaviorandconditionsusingKotlincode.Forexample,youcanapplythestyletoanyButtonbyiteratingthroughacontrol'schildren,filteringforonlychildrenthatareButtons,andapplyingtheaddClass()tothem.

    classMyView:View(){overridevalroot=vbox{button("PressMe")button("PressMeToo")

    children.asSequence().filter{itisButton}.forEach{it.addClass(MyStyle.tackyButton)}}}

    6.TypeSafeCSS

    78

  • Infact,manipulatingclassesonseveralnodesatonceissocommonthatTornadoFXprovidesashortcutforit:

    children.filter{itisButton}.addClass(MyStyle.tackyButton)}

    YoucanalsotargetallButtoninstancesinyourapplicationbyselectingandmodifyingthebuttonintheStylesheet.ThiswillapplythestyletoallButtons.

    importjavafx.scene.paint.Colorimporttornadofx.*

    classMyStyle:Stylesheet(){

    companionobject{valtackyButtonbycssclass()

    privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}

    init{button{rotate=10.degborderColor+=box(topColor,rightColor,leftColor,bottomColor)fontFamily="ComicSansMS"fontSize=20.px}}}

    importjavafx.scene.layout.VBoximporttornadofx.*

    classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}

    classMyView:View(){overridevalroot=vbox{button("PressMe")button("PressMeToo")}}

    6.TypeSafeCSS

    79

  • Figure6.3

    Notealsoyoucanselectmultipleclassesandcontroltypestomix-and-matchstyles.Forexample,youcansetthefontsizeoflabelsandbuttonsto20pixels,andcreatetackybordersandfontsonlyforbuttons(Figure6.4).

    classMyStyle:Stylesheet(){

    companionobject{

    privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}

    init{s(button,label){fontSize=20.px}button{rotate=10.degborderColor+=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,andTornadoFXStylesheetscanstreamlinethiswiththemulti()function.ThisallowsyoutospecifymultiplevaluesviaavarargsparameterandletTornadoFXtakecareoftherest.Forinstance,youcannestmultiplebackgroundcolorsandinsetsintoacontrol(Figure6.5).

    label("LoreIpsum"){style{fontSize=30.pxbackgroundColor=multi(Color.RED,Color.BLUE,Color.YELLOW)backgroundInsets=multi(box(4.px),box(8.px),box(12.px))}}

    Figure6.5

    Themulti()functionshouldworkwherevermultiplevaluesareaccepted.Ifyouwanttoonlyassignasinglevaluetoapropertythatacceptsmultiplevalues,youwillneedtousetheplusAssign()operatortoaddit(Figure6.6).

    6.TypeSafeCSS

    81

  • label("LoreIpsum"){style{fontSize=30.pxbackgroundColor+=Color.REDbackgroundInsets+=box(4.px)}}

    Figure6.6

    NestingStyles

    Insideaselectorblockyoucanapplyfurtherstylestargetingchildcontrols.

    Forinstance,defineaCSSclasscalledcritical.Makeitputanorangeborderaroundanycontrolitisappliedto,andpaditby5pixels.

    classMyStyle:Stylesheet(){

    companionobject{valcriticalbycssclass()}

    init{critical{borderColor+=box(Color.ORANGE)padding=box(5.px)}}}

    Butsupposewhenweappliedcriticaltoanycontrol,suchasanHBox,wewantittoaddadditionalstylingstobuttonsinsidethatcontrol.Nestinganotherselectionwilldothetrick.

    6.TypeSafeCSS

    82

  • classMyStyle:Stylesheet(){companionobject{valcriticalbycssclass()}init{critical{borderColor+=box(Color.ORANGE)padding=box(5.px)button{backgroundColor+=Color.REDtextFill=Color.WHITE}}}}

    Nowwhenyouapplycriticaltosay,anHBox,allbuttonsinsidethatHBoxwillgetthatdefinedstyleforbutton(Figure6.7)

    classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}

    classMyView:View(){overridevalroot=hbox{addClass(MyStyle.critical)button("Warning!")button("Danger!")}}

    Figure6.7

    Thereisonecriticalthingtonotconfusehere.TheorangeborderisonlyappliedtotheHBoxsincethecriticalclasswasappliedtoit.ThebuttonsdonotgetanorangeborderbecausetheyarechildrentotheHBox.Whiletheirstyleisdefinedbycritical,theydonotinheritthestylesoftheirparent,onlythosedefinedforbutton.

    6.TypeSafeCSS

    83

  • Ifyouwantthebuttonstogetanorangebordertoo,youneedtoapplythecriticalclassdirectlytothem.Youwillwanttousetheand()toapplyspecificstylestobuttonsthatarealsodeclaredascritical.

    classMyStyle:Stylesheet(){

    companionobject{valcriticalbycssclass()}

    init{critical{

    borderColor+=box(Color.ORANGE)padding=box(5.px)

    and(button){backgroundColor+=Color.REDtextFill=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

  • NowyouhaveorangebordersaroundtheHBoxaswellasthebuttons.Whennestingstyles,keepinmindthatwrappingtheselectionwithand()willcascadestylestochildrencontrolsorclasses.

    MixinsTherearetimesyoumaywanttoreuseasetofstylingsandapplythemtoseveralcontrolsandselectors.Thispreventsyoufromhavingtoredundantlydefinethesamepropertiesandvalues.Forinstance,ifyouwanttocreateasetofstylingcalledredAllTheThings,youcoulddefineitasamixinasshownbelow.ThenyoucanreuseitforaredStyleclass,aswellasatextInput,alabel,andapasswordFieldwithadditionalstylemodifications(Figure6.9).

    Stylesheet

    6.TypeSafeCSS

    85

  • importjavafx.scene.paint.Colorimportjavafx.scene.text.FontWeightimporttornadofx.*

    classStyles:Stylesheet(){

    companionobject{valredStylebycssclass().}

    init{valredAllTheThings=mixin{backgroundInsets+=box(5.px)borderColor+=box(Color.RED)textFill=Color.RED}

    redStyle{+redAllTheThings}

    s(textInput,label){+redAllTheThingsfontWeight=FontWeight.BOLD}

    passwordField{+redAllTheThingsbackgroundColor+=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

    ThestylesheetisappliedtotheapplicationbyaddingitasaconstructorparametertotheAppclass.Thisisavarargparameter,soyoucansendinacommaseparatedlistofmultiplestylesheets.Ifyouwanttoloadstylesheetsdynamicallybasedonsomecondition,youcancallimportStylesheet(Styles::classfromanywhere.AnyUIComponentopenedafterthecalltoimportStylesheetwillgetthestylesheetapplied.Youcanalsoloadnormaltextbasedcssstylesheetswiththisfunction:

    importStylesheet("/mystyles.css")

    Loadingatextbasedcssstylesheet

    IfyoufindyouarerepeatingyourselfsettingthesameCSSpropertiestothesamevalues,youmightwanttoconsiderusingmixinsandreusingthemwherevertheyareneededinaStylesheet.

    6.TypeSafeCSS

    87

  • ModifierSelectionsTornadoFXalsosupportsmodifierselectionsbyleveragingand()functionswithinaselection.Themostcommoncasethisishandyisstylingfor"selected"andcursor"hover"contextsforacontrol.

    IfyouwantedtocreateaUIthatwillmakeanyButtonredwhenitishoveredover,andanyselectedCellindatacontrolssuchasListViewred,youcandefineaStylesheetlikethis(Figure6.10).

    Stylesheet

    importjavafx.scene.paint.Colorimporttornadofx.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()andoverridevalroot=vbox{button("Hoveroverme")listview(listItems)}}

    6.TypeSafeCSS

    88

  • Figure6.10-AcellisselectedandtheButtonisbeinghoveredover.Botharenowred.

    Wheneveryouneedmodifiers,usetheselect()functiontomakethosecontextualstylemodifications.

    Control-SpecificStylesheetsIfyoudecidetocreateyourowncontrols(oftenbyextendinganexistingcontrol,likeButton),JavaFXallowsyoutopairastylesheetwithit.Inthissituation,itisadvantageoustoloadthisStylesheetonlywhenthiscontrolisloaded.Forinstance,ifyouhaveaDangerButtonclassthatextendsButton,youmightconsidercreatingaStylesheetspecificallyforthatDangerButton.ToallowJavaFXtoloadit,youneedtooverridethegetUserAgentStyleSheet()functionasshownbelow.Thiswillconvertyourtype-safeStylesheetintoplaintextCSSthatJavaFXnativelyunderstands.

    classDangerButton:Button("Danger!"){init{addClass(DangerButtonStyles.dangerButton)}overridefungetUserAgentStylesheet()=DangerButtonStyles().base64URL.toExternalForm()}

    classDangerButtonStyles:Stylesheet(){companionobject{valdangerButtonbycssclass()}

    init{dangerButton{backgroundInsets+=box(0.px)fontWeight=FontWeight.BOLDfontSize=20.pxpadding=box(10.px)}}}

    6.TypeSafeCSS

    89

  • TheDangerButtonStyles().base64URL.toExternalForm()expressioncreatesaninstanceoftheDangerButtonStyles,andturnsitintoaURLcontainingtheentirestylesheetthatJavaFXcanconsume.

    ConclusionTornadoFXdoesagreatjobexecutingabrilliantconcepttomakeCSStype-safe,anditfurtherdemonstratesthepowerofKotlinDSL's.Configurationthroughstatictextfilesisslowtoexpresswith,buttype-safeCSSmakesitfluentandquickespeciallywithIDEauto-completion.EvenifyouarepragmaticaboutUI'sandfeelstylingissuperfluous,therewillbetimesyouneedtoleverageconditionalformattingandhighlightingsorules"popout"inaUI.Atminimum,getcomfortableusingtheinlinestyle{}blocksoyoucanquicklyaccessstylingpropertiesthatcannotbeaccessedanyotherway(suchasTextWeight).

    6.TypeSafeCSS

    90

  • LayoutsandMenusComplexUI'srequiremanycontrols.Itislikelythesecontrolsneedtobegrouped,positioned,andsizedwithsetpolicies.FortunatelyTornadoFXstreamlinesmanylayoutsthatcomewithJavaFX,aswellasfeaturesitsownproprietaryFormlayout.

    TornadoFXalsohastype-safebuilderstocreatemenusinahighlystructured,declarativeway.MenuscanbeespeciallycumbersometobuildusingconventionalJavaFXcode,andKotlinreallyshinesinthisdepartment.

    BuildersforLayoutsLayoutsgroupcontrolsandsetpoliciesabouttheirsizingandpositioningbehavior.Technically,layoutsthemselvesarecontrolssothereforeyoucannestlayoutsinsidelayouts.ThisiscriticalforbuildingcomplexUI's,andTornadoFXmakesmaintenanceofUIcodeeasierbyvisiblyshowingthenestedrelationships.

    VBox

    AVBoxstackscontrolsverticallyintheordertheyaredeclaredinsideitsblock(Figure7.1).

    vbox{button("Button1").setOnAction{println("Button1Pressed")}button("Button2").setOnAction{println("Button2Pressed")}}

    Figure7.1

    YoucanalsocallvboxConstraints()withinachild'sblocktochangethemarginandverticalgrowingbehaviorsoftheVBox.

    7.LayoutsandMenus

    91

  • vbox{button("Button1"){vboxConstraints{marginBottom=20.0vGrow=Priority.ALWAYS}}button("Button2")}

    YoucanuseashorthandextensionpropertyforvGrowwithoutcallingvboxConstraints().

    vbox{button("Button1"){vGrow=Priority.ALWAYS}button("Button2")}

    HBox

    HBoxbehavesalmostidenticallytoVBox,butitstacksallcontrolshorizontallyleft-to-rightintheorderdeclaredinitsblock.

    hbox{button("Button1").setOnAction{println("Button1Pressed")}button("Button2").setOnAction{println("Button2Pressed")}}

    Figure7.2

    Youcanalsocallhboxconstraints()withintheachild'sblocktochangethemarginandhorizontalgrowingbehaviorsoftheHBox.

    7.LayoutsandMenus

    92

  • hbox{button("Button1"){hboxConstraints{marginRight=20.0hGrow=Priority.ALWAYS}}button("Button2")}

    YoucanuseashorthandextensionpropertyforhGrowwithoutcallinghboxConstraints().

    hbox{button("Button1"){hGrow=Priority.ALWAYS}button("Button2")}

    FlowPane

    TheFlowPanelaysoutcontrolsleft-to-rightandwrapstothenextlineontheboundary.Forexample,sayyouadded100buttonstoaFlowPane(Figure7.3).Youwillnoticeitsimplylaysoutbuttonsfromleft-to-right,andwhenitrunsoutofroomitmovestothe"nextline".

    flowpane{for(iin1..100){button(i.toString()){setOnAction{println("Youpressedbutton$i")}}}}

    Figure7.3

    7.LayoutsandMenus

    93