├── .classpath ├── .gitattributes ├── .gitignore ├── .project ├── .scalafmt.conf ├── README.md ├── build.sbt ├── media ├── earthrise.jpg └── omgrobots.mp4 ├── project ├── build.properties └── plugin.sbt └── src └── proscalafx ├── ch01 ├── audioconfig │ ├── AudioConfigMain.scala │ └── AudioConfigModel.scala └── helloearthrise │ ├── HelloEarthRiseMain.scala │ └── HelloScrollPaneMain.scala ├── ch02 ├── metronome1 │ └── Metronome1Main.scala ├── metronomepathtransition │ └── MetronomePathTransitionMain.scala ├── metronometransition │ └── MetronomeTransitionMain.scala ├── onthescene │ ├── OnTheSceneMain.scala │ ├── changeOfScene.css │ └── onTheScene.css ├── stagecoach │ └── StageCoachMain.scala └── zenpong │ ├── ZenPong.scala │ └── ZenPongMain.scala ├── ch03 ├── BidirectionalBindingExample.scala ├── HeronsFormulaDirectExtensionExample.scala ├── HeronsFormulaExample.scala ├── MotivatingExample.scala ├── NumericPropertiesExample.scala ├── RectangleAreaExample.scala ├── TriangleAreaExample.scala ├── TriangleAreaFluentExample.scala └── scalafxbean │ ├── ScalaFXBeanControllerExample.scala │ ├── ScalaFXBeanMainExample.scala │ ├── ScalaFXBeanModelExample.scala │ └── ScalaFXBeanViewExample.scala ├── ch04 └── reversi │ ├── examples │ ├── AlignUsingStackAndTile.scala │ ├── BorderLayoutExample.scala │ ├── CenterUsingBind.scala │ ├── CenterUsingStack.scala │ └── PlayerScoreExample.scala │ ├── model │ ├── Owner.scala │ └── ReversiModel.scala │ ├── reversipieces │ ├── ReversiPieceTest.scala │ └── ReversiSquareTest.scala │ └── ui │ ├── Reversi.scala │ ├── ReversiApp.scala │ ├── ReversiPiece.scala │ └── ReversiSquare.scala ├── ch05 ├── model │ ├── Person.scala │ └── StarterAppModel.scala └── ui │ ├── StarterAppMain.scala │ ├── images │ └── paper.png │ └── starterApp.css ├── ch06 ├── BufferChangeEventExample.scala ├── FXCollectionsExample.scala ├── JavaFXSceneInSwingExample.scala ├── JavaFXThreadsExample.scala ├── MapChangeEventExample.scala ├── ObservableBufferExample.scala ├── ResponsiveUIExample.scala ├── ServiceExample.scala ├── SetChangeEventExample.scala ├── UnresponsiveUIExample.scala └── WorkerAndTaskExample.scala ├── ch07 ├── ChartApp1.scala ├── ChartApp10.scala ├── ChartApp11.scala ├── ChartApp2.scala ├── ChartApp3.scala ├── ChartApp4.scala ├── ChartApp5.scala ├── ChartApp6.scala ├── ChartApp7.scala ├── ChartApp8.scala └── ChartApp9.scala ├── ch08 ├── AudioPlayer1 │ ├── AudioPlayer1.scala │ └── resources │ │ └── keeper.mp3 ├── AudioPlayer2 │ ├── AudioPlayer2.scala │ ├── media.css │ └── resources │ │ ├── cross.png │ │ └── defaultAlbum.png ├── AudioPlayer3 │ ├── AbstractView.scala │ ├── AudioPlayer3.scala │ ├── MetadataView.scala │ ├── PlayerControlsView.scala │ ├── SongModel.scala │ ├── media.css │ └── resources │ │ ├── cross.png │ │ ├── defaultAlbum.png │ │ ├── music_note.png │ │ ├── next.png │ │ ├── pause.png │ │ ├── play.png │ │ ├── prev.png │ │ ├── volHigh.png │ │ └── volLow.png ├── AudioPlayer4 │ ├── AbstractView.scala │ ├── AudioPlayer4.scala │ ├── EqualizerView.scala │ ├── MetadataView.scala │ ├── PlayerControlsView.scala │ ├── SongModel.scala │ ├── SpectrumBar.scala │ ├── SpectrumListener.scala │ ├── media.css │ └── resources │ │ ├── cross.png │ │ ├── defaultAlbum.png │ │ ├── music_note.png │ │ ├── next.png │ │ ├── pause.png │ │ ├── play.png │ │ ├── prev.png │ │ ├── volHigh.png │ │ └── volLow.png ├── BasicAudioClip │ ├── BasicAudioClip.scala │ ├── media.css │ └── resources │ │ ├── beep.wav │ │ └── cross.png ├── CodeMonkeyToDo │ ├── CodeMonkeyToDo.scala │ ├── media.css │ └── resources │ │ ├── coffee.mp3 │ │ ├── coffee.wav │ │ ├── cross.png │ │ ├── job.mp3 │ │ ├── job.wav │ │ ├── meeting.mp3 │ │ └── meeting.wav ├── FullScreenVideoPlayer │ └── FullScreenVideoPlayer.scala ├── VideoPlayer1 │ └── VideoPlayer1.scala ├── VideoPlayer2 │ ├── VideoPlayer2.scala │ └── media.css ├── VideoPlayer3 │ ├── VideoPlayer3.scala │ └── media.css └── VideoPlayer4 │ ├── AbstractView.scala │ ├── EqualizerView.scala │ ├── MediaModel.scala │ ├── PlayerControlsView.scala │ ├── SpectrumBar.scala │ ├── SpectrumListener.scala │ ├── VideoPlayer4.scala │ ├── VideoPlayer4App.scala │ ├── VideoView.scala │ ├── media.css │ └── resources │ ├── PlayPause.png │ ├── cross.png │ ├── defaultAlbum.png │ ├── filmstrip.png │ ├── music_note.png │ ├── next.png │ ├── pause.png │ ├── play.png │ ├── prev.png │ ├── thumb.png │ ├── volHigh.png │ └── volLow.png └── ch10 └── fxml ├── AdoptionForm.fxml ├── AdoptionFormController.scala ├── FXMLAdoptionForm.scala └── cat.jpg /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default line separator behavior, in case people don't have core.autocrlf set. 2 | # [[https://stackoverflow.com/a/42135910]] 3 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | 4 | *.pydevproject 5 | .project 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .classpath 15 | .settings/ 16 | .loadpath 17 | # External tool builders 18 | .externalToolBuilders/ 19 | # Locally stored "Eclipse launch configurations" 20 | *.launch 21 | # CDT-specific 22 | .cproject 23 | # PDT-specific 24 | .buildpath 25 | ## Visual Studio 26 | ## Ignore Visual Studio temporary files, build results, and 27 | ## files generated by popular Visual Studio add-ons. 28 | # User-specific files 29 | *.suo 30 | *.user 31 | *.sln.docstates 32 | # Build results 33 | [Dd]ebug/ 34 | [Rr]elease/ 35 | *_i.c 36 | *_p.c 37 | *.ilk 38 | *.meta 39 | *.obj 40 | *.pch 41 | *.pdb 42 | *.pgc 43 | *.pgd 44 | *.rsp 45 | *.sbr 46 | *.tlb 47 | *.tli 48 | *.tlh 49 | *.vspscc 50 | .builds 51 | *.dotCover 52 | ## If you have NuGet Package Restore enabled, uncomment this 53 | #packages/ 54 | # Visual C++ cache files 55 | ipch/ 56 | *.aps 57 | *.ncb 58 | *.opensdf 59 | *.sdf 60 | # Visual Studio profiler 61 | *.psess 62 | *.vsp 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper* 65 | # Installshield output folder 66 | [Ee]xpress 67 | # DocProject is a documentation generator add-in 68 | DocProject/buildhelp/ 69 | DocProject/Help/*.HxT 70 | DocProject/Help/*.HxC 71 | DocProject/Help/*.hhc 72 | DocProject/Help/*.hhk 73 | DocProject/Help/*.hhp 74 | DocProject/Help/Html2 75 | DocProject/Help/html 76 | # Click-Once directory 77 | publish 78 | # Others 79 | [Bb]in 80 | [Oo]bj 81 | sql 82 | TestResults 83 | *.Cache 84 | ClientBin 85 | stylecop.* 86 | ~$* 87 | *.dbmdl 88 | Generated_Code #added for RIA/Silverlight projects 89 | # Backup & report files from converting an old project file to a newer 90 | # Visual Studio version. Backup files are not needed, because we have git ;-) 91 | _UpgradeReport_Files/ 92 | Backup*/ 93 | UpgradeLog*.XML 94 | ############ 95 | ## Windows 96 | # Windows image file caches 97 | Thumbs.db 98 | # Folder config file 99 | Desktop.ini 100 | ############# 101 | ## Python 102 | *.py[co] 103 | # Packages 104 | *.egg 105 | *.egg-info 106 | dist 107 | build 108 | eggs 109 | parts 110 | bin 111 | var 112 | sdist 113 | develop-eggs 114 | .installed.cfg 115 | # Installer logs 116 | pip-log.txt 117 | # Unit test / coverage reports 118 | .coverage 119 | .tox 120 | #Translations 121 | *.mo 122 | #Mr Developer 123 | .mr.developer.cfg 124 | # Mac crap 125 | .DS_Store 126 | .scala_dependencies 127 | /.idea/ 128 | /local/ 129 | /out/ 130 | /target/ 131 | /.idea_modules/ 132 | target 133 | /.bsp/ 134 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProScalaFX 4 | 5 | 6 | 7 | 8 | 9 | org.scala-ide.sdt.core.scalabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.xtext.ui.shared.xtextBuilder 15 | 16 | 17 | 18 | 19 | 20 | org.scala-ide.sdt.core.scalanature 21 | org.eclipse.jdt.core.javanature 22 | org.eclipse.xtext.ui.shared.xtextNature 23 | 24 | 25 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.4 2 | 3 | runner.dialect = scala213source3 4 | 5 | preset = IntelliJ 6 | align.preset = more 7 | maxColumn = 120 8 | docstrings.style = Asterisk 9 | docstrings.blankFirstLine = yes 10 | docstrings.wrap = no 11 | importSelectors = singleLine 12 | newlines.source = keep -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ProScalaFX 2 | ========== 3 | 4 | [ScalaFX][1] version of Pro JavaFX books source code. 5 | 6 | Book editions: 7 | 8 | * [Pro JavaFX 9][b9p], book source [code][b9c] 9 | * [Pro JavaFX 8][b8p], book source [code][b8c]. 10 | * [Pro JavaFX 2][b2p], book source [code][b2c]. 11 | 12 | The original code was created for JavaFX 2.2 (Java 7). Current main branch was updated to work with ScalaFX 11 / JavaFX 13 | 14 | 11. ScalaFX 8 is still available on branch [SFX-8][sfx8]. Code for ScalaFX 2 is on branch [SFX-2][sfx2]. 15 | 16 | Please use [ScalaFX Users Group][5] to post questions. 17 | 18 | 19 | [1]: http://scalafx.org 20 | 21 | [5]: https://groups.google.com/forum/#!forum/scalafx-users 22 | 23 | [sfx2]: https://github.com/scalafx/ProScalaFX/tree/SFX-2 24 | 25 | [sfx8]: https://github.com/scalafx/ProScalaFX/tree/SFX-8 26 | 27 | [b2c]: http://www.apress.com/downloadable/download/sample/sample_id/1253 28 | 29 | [b2p]: http://www.apress.com/9781430268727 30 | 31 | [b8c]: http://www.apress.com/downloadable/download/sample/sample_id/1574 32 | 33 | [b8p]: http://www.apress.com/9781430265740 34 | 35 | [b9c]: https://github.com/Apress/pro-javafx-9 36 | 37 | [b9p]: https://www.apress.com/us/book/9781484230411 38 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | // Project name 2 | name := "ProScalaFX" 3 | 4 | // Current version 5 | version := "24.0.0-R35" 6 | 7 | // Version of scala to use 8 | val scala2Version = "2.13.16" 9 | val scala3Version = "3.3.5" 10 | // To cross-compile with Scala 2 and Scala 3 11 | crossScalaVersions := Seq(scala2Version, scala3Version) 12 | scalaVersion := scala3Version 13 | 14 | // Set the main Scala source directory to be /src 15 | Compile / scalaSource := baseDirectory(_ / "src").value 16 | 17 | Compile / resourceDirectory := baseDirectory(_ / "src").value 18 | 19 | // Append -deprecation to the options passed to the Scala compiler 20 | scalacOptions ++= Seq("-deprecation", "-feature") 21 | scalacOptions ++= { 22 | CrossVersion.partialVersion(scalaVersion.value) match { 23 | case Some((2, _)) => Seq("-Xcheckinit", "-Xsource:3") 24 | case Some((3, _)) => Seq("-rewrite", "-source:3.3-migration", "-explain", "-explain-types") 25 | case _ => Seq.empty[String] 26 | } 27 | } 28 | 29 | // Point to location of a snapshot repository for ScalaFX 30 | resolvers ++= Opts.resolver.sonatypeOssSnapshots 31 | 32 | //resolvers += Opts.resolver.sonatypeStaging 33 | 34 | // Add ScalaFX dependency, exclude JavaFX transitive dependencies, may not match this OS 35 | libraryDependencies += "org.scalafx" %% "scalafx" % "24.0.0-R35" 36 | 37 | // Fork a new JVM for 'run' and 'test:run' 38 | fork := true 39 | -------------------------------------------------------------------------------- /media/earthrise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/media/earthrise.jpg -------------------------------------------------------------------------------- /media/omgrobots.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/media/omgrobots.mp4 -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | 3 | -------------------------------------------------------------------------------- /project/plugin.sbt: -------------------------------------------------------------------------------- 1 | scalacOptions ++= Seq("-unchecked", "-deprecation") 2 | -------------------------------------------------------------------------------- /src/proscalafx/ch01/audioconfig/AudioConfigMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch01.audioconfig 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.VPos 7 | import scalafx.scene.Scene 8 | import scalafx.scene.control.{CheckBox, ChoiceBox, Slider} 9 | import scalafx.scene.paint.{Color, LinearGradient, Stop} 10 | import scalafx.scene.shape.{Line, Rectangle} 11 | import scalafx.scene.text.{Font, FontWeight, Text} 12 | 13 | object AudioConfigMain extends JFXApp3 { 14 | 15 | override def start(): Unit = { 16 | 17 | val acModel = new AudioConfigModel() 18 | 19 | val genreChoiceBox = new ChoiceBox[String] { 20 | layoutX = 204 21 | layoutY = 154 22 | prefWidth = 93 23 | items = acModel.genres 24 | } 25 | 26 | stage = new PrimaryStage { 27 | title = "Audio Configuration" 28 | scene = new Scene { 29 | content = List( 30 | new Rectangle { 31 | width = 320 32 | height = 45 33 | fill = new LinearGradient( 34 | endX = 0.0, 35 | endY = 1.0, 36 | stops = List( 37 | Stop(0, Color.web("0xAEBBCC")), 38 | Stop(1, Color.web("0x6D84A3")) 39 | ) 40 | ) 41 | }, 42 | new Text { 43 | layoutX = 65 44 | layoutY = 12 45 | textOrigin = VPos.Top 46 | fill = Color.White 47 | text = "Audio Configuration" 48 | font = Font.font("SansSerif", FontWeight.Bold, 20) 49 | }, 50 | new Rectangle { 51 | x = 0 52 | y = 43 53 | width = 320 54 | height = 300 55 | fill = Color.rgb(199, 206, 213) 56 | }, 57 | new Rectangle { 58 | x = 9 59 | y = 54 60 | width = 300 61 | height = 130 62 | arcWidth = 20 63 | arcHeight = 20 64 | fill = Color.White 65 | stroke = Color.color(0.66, 0.67, 0.69) 66 | }, 67 | new Text { 68 | layoutX = 18 69 | layoutY = 69 70 | textOrigin = VPos.Top 71 | fill = Color.web("#131021") 72 | font = Font.font("SansSerif", FontWeight.Bold, 18) 73 | text <== acModel.selectedDBs.asString + " dB" 74 | }, 75 | new Slider { 76 | layoutX = 135 77 | layoutY = 69 78 | prefWidth = 162 79 | min = acModel.minDecibels 80 | max = acModel.maxDecibels 81 | value <==> acModel.selectedDBs 82 | disable <== acModel.muting 83 | }, 84 | new Line { 85 | startX = 9 86 | startY = 97 87 | endX = 309 88 | endY = 97 89 | stroke = Color.color(0.66, 0.67, 0.69) 90 | }, 91 | new Text { 92 | layoutX = 18 93 | layoutY = 113 94 | textOrigin = VPos.Top 95 | fill = Color.web("#131021") 96 | text = "Muting" 97 | font = Font.font("SanSerif", FontWeight.Bold, 18) 98 | }, 99 | new CheckBox { 100 | layoutX = 280 101 | layoutY = 113 102 | selected <==> acModel.muting 103 | }, 104 | new Line { 105 | startX = 9 106 | startY = 141 107 | endX = 309 108 | endY = 141 109 | stroke = Color.color(0.66, 0.67, 0.69) 110 | }, 111 | new Text { 112 | layoutX = 18 113 | layoutY = 154 114 | textOrigin = VPos.Top 115 | fill = Color.web("#131021") 116 | text = "Genre" 117 | font = Font.font("SanSerif", FontWeight.Bold, 18) 118 | }, 119 | genreChoiceBox 120 | ) 121 | } 122 | } 123 | 124 | acModel.genreSelectionModel = genreChoiceBox.selectionModel() 125 | acModel.addListenerToGenreSelectionModel() 126 | acModel.genreSelectionModel.selectFirst() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/proscalafx/ch01/audioconfig/AudioConfigModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch01.audioconfig 2 | 3 | import scalafx.beans.property.{BooleanProperty, IntegerProperty} 4 | import scalafx.collections.ObservableBuffer 5 | import scalafx.scene.control.SingleSelectionModel 6 | 7 | class AudioConfigModel { 8 | 9 | /** The minimum audio volume in decibels */ 10 | val minDecibels = 0.0 11 | 12 | /** The maximum audio volume in decibels */ 13 | val maxDecibels = 160.0 14 | 15 | /** The selected audio volume in decibels */ 16 | val selectedDBs = IntegerProperty(0) 17 | 18 | /** Indicates whether audio is muted */ 19 | val muting = BooleanProperty(false) 20 | 21 | /** 22 | * List of some musical genres 23 | */ 24 | val genres = ObservableBuffer("Chamber", "Country", "Cowbell", "Metal", "Polka", "Rock") 25 | 26 | /** A reference to the selection model used by the Slider */ 27 | var genreSelectionModel: SingleSelectionModel[String] = _ 28 | 29 | /** 30 | * Adds a change listener to the selection model of the ChoiceBox, and contains 31 | * code that executes when the selection in the ChoiceBox changes. 32 | */ 33 | def addListenerToGenreSelectionModel(): Unit = { 34 | this.genreSelectionModel.selectedIndex.onChange { 35 | selectedDBs.value = this.genreSelectionModel.selectedIndex() match { 36 | case 0 => 80 37 | case 1 => 100 38 | case 2 => 150 39 | case 3 => 140 40 | case 4 => 120 41 | case 5 => 130 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/proscalafx/ch01/helloearthrise/HelloEarthRiseMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch01.helloearthrise 2 | 3 | import scalafx.animation.{Interpolator, Timeline, TranslateTransition} 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.VPos 7 | import scalafx.scene.image.{Image, ImageView} 8 | import scalafx.scene.paint.Color 9 | import scalafx.scene.shape.Rectangle 10 | import scalafx.scene.text.{Font, FontWeight, Text, TextAlignment} 11 | import scalafx.scene.{Group, Scene} 12 | import scalafx.util.Duration 13 | 14 | import java.io.File 15 | 16 | /** Main class for the "Hello World" style example. */ 17 | object HelloEarthRiseMain extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | 21 | val message = 22 | """Earthrise at Christmas: 23 | |[Forty] years ago this Christmas, a turbulent world 24 | |looked to the heavens for a unique view of our home 25 | |planet. This photo of Earthrise over the lunar horizon 26 | |was taken by the Apollo 8 crew in December 1968, showing 27 | |Earth for the first time as it appears from deep space. 28 | |Astronauts Frank Borman, Jim Lovell and William Anders 29 | |had become the first humans to leave Earth orbit, 30 | |entering lunar orbit on Christmas Eve. In a historic live 31 | |broadcast that night, the crew took turns reading from 32 | |the Book of Genesis, closing with a holiday wish from 33 | |Commander Borman: "We close with good night, good luck, 34 | |a Merry Christmas, and God bless all of you -- all of 35 | |you on the good Earth." """.stripMargin 36 | 37 | val textRef = new Text { 38 | layoutY = 100 39 | textOrigin = VPos.Top 40 | textAlignment = TextAlignment.Justify 41 | wrappingWidth = 400 42 | text = message 43 | fill = Color.rgb(187, 195, 107) 44 | font = Font.font("SansSerif", FontWeight.Bold, 24.0) 45 | } 46 | 47 | stage = new PrimaryStage { 48 | title = "Hello Earthrise" 49 | scene = new Scene(516, 387) { 50 | content = List( 51 | new ImageView(image = new Image(new File("media/earthrise.jpg").toURI.toString)), 52 | new Group { 53 | layoutX = 50 54 | layoutY = 180 55 | children = List(textRef) 56 | clip = Rectangle(430, 85) 57 | } 58 | ) 59 | } 60 | } 61 | 62 | new TranslateTransition { 63 | cycleCount = Timeline.Indefinite 64 | duration = Duration(75000) 65 | node = textRef 66 | toY = -820 67 | interpolator = Interpolator.Linear 68 | }.play() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/proscalafx/ch01/helloearthrise/HelloScrollPaneMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch01.helloearthrise 2 | 3 | import scalafx.animation.{Interpolator, Timeline, TranslateTransition} 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.VPos 7 | import scalafx.scene.Scene 8 | import scalafx.scene.control.ScrollPane 9 | import scalafx.scene.control.ScrollPane.ScrollBarPolicy 10 | import scalafx.scene.image.{Image, ImageView} 11 | import scalafx.scene.paint.Color 12 | import scalafx.scene.text.{Font, FontWeight, Text, TextAlignment} 13 | import scalafx.util.Duration 14 | 15 | import java.io.File 16 | 17 | object HelloScrollPaneMain extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | val message = 21 | """Earthrise at Christmas: 22 | [Forty] years ago this Christmas, a turbulent world 23 | looked to the heavens for a unique view of our home 24 | planet. This photo of Earthrise over the lunar horizon 25 | was taken by the Apollo 8 crew in December 1968, showing 26 | Earth for the first time as it appears from deep space. 27 | Astronauts Frank Borman, Jim Lovell and William Anders 28 | had become the first humans to leave Earth orbit, 29 | entering lunar orbit on Christmas Eve. In a historic live 30 | broadcast that night, the crew took turns reading from 31 | the Book of Genesis, closing with a holiday wish from 32 | Commander Borman: "We close with good night, good luck, 33 | a Merry Christmas, and God bless all of you -- all of 34 | you on the good Earth." """.replace("\n", "") 35 | 36 | val textRef = new Text { 37 | layoutY = 100 38 | textOrigin = VPos.Top 39 | textAlignment = TextAlignment.Justify 40 | wrappingWidth = 400 41 | text = message 42 | fill = Color.rgb(187, 195, 107) 43 | font = Font.font("SansSerif", FontWeight.Bold, 24.0) 44 | } 45 | 46 | stage = new PrimaryStage { 47 | title = "Hello Earthrise" 48 | scene = new Scene(516, 387) { 49 | content = List( 50 | new ImageView(new Image(new File("media/earthrise.jpg").toURI.toString)), 51 | new ScrollPane { 52 | layoutX = 50 53 | layoutY = 180 54 | prefWidth = 440 55 | prefHeight = 85 56 | hbarPolicy = ScrollBarPolicy.Never 57 | vbarPolicy = ScrollBarPolicy.Never 58 | pannable = true 59 | content = textRef 60 | style = "-fx-background-color: transparent;" 61 | } 62 | ) 63 | } 64 | } 65 | 66 | new TranslateTransition { 67 | cycleCount = Timeline.Indefinite 68 | duration = Duration(75000) 69 | node = textRef 70 | toY = -820 71 | interpolator = Interpolator.Linear 72 | }.play() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/metronome1/Metronome1Main.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch02.metronome1 2 | 3 | import javafx.animation.Animation.Status 4 | import scalafx.Includes.* 5 | import scalafx.animation.{Interpolator, Timeline} 6 | import scalafx.application.JFXApp3 7 | import scalafx.application.JFXApp3.PrimaryStage 8 | import scalafx.beans.property.DoubleProperty 9 | import scalafx.scene.Scene 10 | import scalafx.scene.control.Button 11 | import scalafx.scene.layout.HBox 12 | import scalafx.scene.paint.Color 13 | import scalafx.scene.shape.Line 14 | 15 | import scala.language.postfixOps 16 | 17 | object Metronome1Main extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | 21 | val startXVal = DoubleProperty(100.0) 22 | 23 | val anim = new Timeline { 24 | autoReverse = true 25 | keyFrames = Seq( 26 | at(0 s) { 27 | startXVal -> 100 28 | }, 29 | at(1 s) { 30 | startXVal -> 300 tween Interpolator.Linear 31 | } 32 | ) 33 | cycleCount = Timeline.Indefinite 34 | } 35 | 36 | stage = new PrimaryStage { 37 | title = "Metronome 1" 38 | scene = new Scene(400, 500) { 39 | content = List( 40 | new Line { 41 | startX <== startXVal 42 | startY = 50 43 | endX = 200 44 | endY = 400 45 | strokeWidth = 4 46 | stroke = Color.Blue 47 | }, 48 | new HBox { 49 | layoutX = 60 50 | layoutY = 420 51 | spacing = 10 52 | children = List( 53 | new Button { 54 | text = "Start" 55 | onAction = () => anim.playFromStart() 56 | disable <== anim.status =!= Status.STOPPED 57 | }, 58 | new Button { 59 | text = "Pause" 60 | onAction = () => anim.pause() 61 | disable <== anim.status =!= Status.RUNNING 62 | }, 63 | new Button { 64 | text = "Resume" 65 | onAction = () => anim.play() 66 | disable <== anim.status =!= Status.PAUSED 67 | }, 68 | new Button { 69 | text = "Stop" 70 | onAction = () => anim.stop() 71 | disable <== anim.status === Status.STOPPED 72 | } 73 | ) 74 | } 75 | ) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/metronomepathtransition/MetronomePathTransitionMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch02.metronomepathtransition 2 | 3 | import scalafx.Includes.* 4 | import scalafx.animation.Animation.Status 5 | import scalafx.animation.PathTransition.OrientationType 6 | import scalafx.animation.{Interpolator, PathTransition, Timeline} 7 | import scalafx.application.JFXApp3 8 | import scalafx.application.JFXApp3.PrimaryStage 9 | import scalafx.scene.Scene 10 | import scalafx.scene.control.Button 11 | import scalafx.scene.layout.HBox 12 | import scalafx.scene.paint.Color 13 | import scalafx.scene.shape.{ArcTo, Ellipse, MoveTo, Path} 14 | import scalafx.util.Duration 15 | 16 | object MetronomePathTransitionMain extends JFXApp3 { 17 | 18 | override def start(): Unit = { 19 | val ellipse = new Ellipse { 20 | centerX = 100 21 | centerY = 50 22 | radiusX = 4 23 | radiusY = 8 24 | fill = Color.Blue 25 | } 26 | 27 | val anim = new PathTransition { 28 | duration = Duration(1000.0) 29 | node = ellipse 30 | path = new Path { 31 | elements = List( 32 | MoveTo(100, 50), 33 | ArcTo(350, 350, 0, 300, 50, largeArcFlag = false, sweepFlag = true) 34 | ) 35 | } 36 | orientation = OrientationType.OrthogonalToTangent 37 | interpolator = Interpolator.Linear 38 | autoReverse = true 39 | cycleCount = Timeline.Indefinite 40 | } 41 | 42 | stage = new PrimaryStage { 43 | title = "Metronome using PathTransition" 44 | scene = new Scene(400, 500) { 45 | content = List( 46 | ellipse, 47 | new HBox { 48 | layoutX = 60 49 | layoutY = 420 50 | spacing = 10 51 | // NOTE: the `disable` bindings below compare value of a property to JavaFX constant 52 | children = List( 53 | new Button { 54 | text = "Start" 55 | onAction = () => anim.playFromStart() 56 | disable <== (anim.status =!= Status.Stopped.delegate) 57 | }, 58 | new Button { 59 | text = "Pause" 60 | onAction = () => anim.pause() 61 | disable <== (anim.status =!= Status.Running.delegate) 62 | }, 63 | new Button { 64 | text = "Resume" 65 | onAction = () => anim.play() 66 | disable <== (anim.status =!= Status.Paused.delegate) 67 | }, 68 | new Button { 69 | text = "Stop" 70 | onAction = () => anim.stop() 71 | disable <== (anim.status === Status.Stopped.delegate) 72 | } 73 | ) 74 | } 75 | ) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/metronometransition/MetronomeTransitionMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch02.metronometransition 2 | 3 | import javafx.animation.Animation.Status 4 | import scalafx.Includes.* 5 | import scalafx.animation.{Interpolator, Timeline, TranslateTransition} 6 | import scalafx.application.JFXApp3 7 | import scalafx.application.JFXApp3.PrimaryStage 8 | import scalafx.scene.Scene 9 | import scalafx.scene.control.Button 10 | import scalafx.scene.layout.HBox 11 | import scalafx.scene.paint.Color 12 | import scalafx.scene.shape.Circle 13 | import scalafx.util.Duration 14 | 15 | object MetronomeTransitionMain extends JFXApp3 { 16 | override def start(): Unit = { 17 | 18 | val circle = new Circle { 19 | centerX = 100 20 | centerY = 50 21 | radius = 4 22 | fill = Color.Blue 23 | } 24 | 25 | val anim = new TranslateTransition { 26 | duration = Duration(1000.0) 27 | node = circle 28 | fromX = 0 29 | toX = 200 30 | interpolator = Interpolator.Linear 31 | autoReverse = true 32 | cycleCount = Timeline.Indefinite 33 | } 34 | 35 | stage = new PrimaryStage { 36 | width = 400 37 | height = 500 38 | title = "Metronome using TranslateTransition" 39 | scene = new Scene(400, 500) { 40 | content = List( 41 | circle, 42 | new HBox { 43 | layoutX = 60 44 | layoutY = 420 45 | spacing = 10 46 | children = List( 47 | new Button { 48 | text = "Start" 49 | onAction = () => anim.playFromStart() 50 | disable <== anim.status.isNotEqualTo(Status.STOPPED) 51 | }, 52 | new Button { 53 | text = "Pause" 54 | onAction = () => anim.pause() 55 | disable <== anim.status.isNotEqualTo(Status.RUNNING) 56 | }, 57 | new Button { 58 | text = "Resume" 59 | onAction = () => anim.play() 60 | disable <== anim.status.isNotEqualTo(Status.PAUSED) 61 | }, 62 | new Button { 63 | text = "Stop" 64 | onAction = () => anim.stop() 65 | disable <== anim.status.isEqualTo(Status.STOPPED) 66 | } 67 | ) 68 | } 69 | ) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/onthescene/changeOfScene.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011, Pro JavaFX Authors 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 1. Redistributions of source code must retain the above copyright notice, 8 | * this list of conditions and the following disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, 10 | * this list of conditions and the following disclaimer in the documentation 11 | * and/or other materials provided with the distribution. 12 | * 3. Neither the name of JFXtras nor the names of its contributors may be used 13 | * to endorse or promote products derived from this software without 14 | * specific prior written permission. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | * POSSIBILITY OF SUCH DAMAGE. 27 | * 28 | * changeOfScene.css - A CSS (Cascading Style Sheet) file the is referred to by 29 | * the OnTheSceneMain.fx program to apply styles to the scene and its nodes. 30 | * Note: Syntax recommendation http://www.w3.org/TR/REC-CSS2/ 31 | * 32 | * Developed 2011 by James L. Weaver jim.weaver [at] javafxpert.com 33 | * as a JavaFX SDK 2.0 example for the Pro JavaFX book. 34 | */ 35 | 36 | #stageX, #stageY { 37 | -fx-padding: 3; 38 | -fx-border-color: blue; 39 | -fx-stroke-dash-array: 12 2 4 2; 40 | -fx-border-width: 4; 41 | -fx-border-radius: 5; 42 | } 43 | 44 | .emphasized-text { 45 | -fx-font-size: 14pt; 46 | -fx-font-weight: bold; 47 | -fx-font-style: normal; 48 | } 49 | 50 | .radio-button *.radio { 51 | -fx-padding: 10; 52 | -fx-background-color: red, yellow; 53 | -fx-background-insets: 0, 5; 54 | -fx-background-radius: 30, 20; 55 | } 56 | 57 | .radio-button:focused *.radio { 58 | -fx-background-color: blue, red, yellow; 59 | -fx-background-insets: -5, 0, 5; 60 | -fx-background-radius: 40, 30, 20; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/onthescene/onTheScene.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011, Pro JavaFX Authors 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 1. Redistributions of source code must retain the above copyright notice, 8 | * this list of conditions and the following disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, 10 | * this list of conditions and the following disclaimer in the documentation 11 | * and/or other materials provided with the distribution. 12 | * 3. Neither the name of JFXtras nor the names of its contributors may be used 13 | * to endorse or promote products derived from this software without 14 | * specific prior written permission. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | * POSSIBILITY OF SUCH DAMAGE. 27 | * 28 | * onTheScene.css - A CSS (Cascading Style Sheet) file the is referred to by 29 | * the OnTheSceneMain.fx program to apply styles to the scene and its nodes. 30 | * Note: Syntax recommendation http://www.w3.org/TR/REC-CSS2/ 31 | * 32 | * Developed 2011 by James L. Weaver jim.weaver [at] javafxpert.com 33 | * as a JavaFX SDK 2.0 example for the Pro JavaFX book. 34 | */ 35 | 36 | #stageX, #stageY { 37 | -fx-padding: 1; 38 | -fx-border-color: black; 39 | -fx-border-style: dashed; 40 | -fx-border-width: 2; 41 | -fx-border-radius: 5; 42 | } 43 | 44 | .emphasized-text { 45 | -fx-font-size: 14pt; 46 | -fx-font-weight: normal; 47 | -fx-font-style: italic; 48 | } 49 | 50 | .choice-box:hover { 51 | -fx-scale-x: 1.1; 52 | -fx-scale-y: 1.1; 53 | } 54 | 55 | .radio-button .radio { 56 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, 57 | -fx-inner-border, -fx-body-color; 58 | -fx-background-insets: 0 0 -1 0, 0, 1, 2; 59 | -fx-background-radius: 1.0em; 60 | -fx-padding: 0.333333em; 61 | } 62 | 63 | .radio-button:focused .radio { 64 | -fx-background-color: -fx-focus-color, -fx-outer-border, 65 | -fx-inner-border, -fx-body-color; 66 | -fx-background-radius: 1.0em; 67 | -fx-background-insets: -1.4, 0, 1, 2; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/stagecoach/StageCoachMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch02.stagecoach 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.beans.property.StringProperty 7 | import scalafx.geometry.VPos 8 | import scalafx.scene.control.{Button, CheckBox, Label, TextField} 9 | import scalafx.scene.layout.{HBox, VBox} 10 | import scalafx.scene.paint.Color 11 | import scalafx.scene.shape.Rectangle 12 | import scalafx.scene.text.Text 13 | import scalafx.scene.{Group, Scene} 14 | import scalafx.stage.{Screen, StageStyle} 15 | 16 | /** 17 | * Stage property example. 18 | * 19 | * Can be run with various command line parameters to control stage style: 20 | * decorated - a solid white background and platform decorations (default). 21 | * transparent - transparent background and no decorations. 22 | * undecorated - a solid white background and no decorations. 23 | * utility - a solid white background and minimal platform decorations used for a utility window. 24 | * 25 | * @author Rafael 26 | */ 27 | object StageCoachMain extends JFXApp3 { 28 | 29 | override def start(): Unit = { 30 | 31 | val titleProperty = StringProperty("") 32 | 33 | // Process command line parameters 34 | val stageStyle = parameters.unnamed match { 35 | case Seq("transparent") => StageStyle.Transparent 36 | case Seq("undecorated") => StageStyle.Undecorated 37 | case Seq("utility") => StageStyle.Utility 38 | case _ => StageStyle.Decorated 39 | } 40 | 41 | val textStageX = new Text { 42 | textOrigin = VPos.Top 43 | } 44 | val textStageY = new Text { 45 | textOrigin = VPos.Top 46 | } 47 | val textStageW = new Text { 48 | textOrigin = VPos.Top 49 | } 50 | val textStageH = new Text { 51 | textOrigin = VPos.Top 52 | } 53 | val textStageF = new Text { 54 | textOrigin = VPos.Top 55 | } 56 | val checkBoxResizable = new CheckBox { 57 | text = "resizable" 58 | disable = stageStyle == StageStyle.Transparent || stageStyle == StageStyle.Undecorated 59 | } 60 | val checkBoxFullScreen = new CheckBox { 61 | text = "fullScreen" 62 | } 63 | val titleTextField = new TextField { 64 | text = "Stage Coach" 65 | } 66 | 67 | stage = new PrimaryStage { 68 | resizable = false 69 | title <== titleProperty 70 | scene = new Scene(270, 370) { 71 | fill = Color.Transparent 72 | content = new Group { 73 | children = List( 74 | new Rectangle { 75 | width = 250 76 | height = 350 77 | arcWidth = 50 78 | arcHeight = 50 79 | fill = Color.SkyBlue 80 | }, 81 | new VBox { 82 | layoutX = 30 83 | layoutY = 20 84 | spacing = 10 85 | children = List( 86 | textStageX, 87 | textStageY, 88 | textStageW, 89 | textStageH, 90 | textStageF, 91 | checkBoxResizable, 92 | checkBoxFullScreen, 93 | new HBox { 94 | spacing = 10 95 | children = List( 96 | new Label("title:"), 97 | titleTextField 98 | ) 99 | }, 100 | new Button { 101 | text = "toBack()" 102 | onAction = () => stage.toBack() 103 | }, 104 | new Button { 105 | text = "toFront()" 106 | onAction = () => stage.toFront() 107 | }, 108 | new Button { 109 | text = "close()" 110 | onAction = () => stage.close() 111 | } 112 | ) 113 | } 114 | ) 115 | } 116 | } 117 | } 118 | 119 | // when mouse button is pressed, save the initial position of screen 120 | val rootGroup = stage.scene().content(0) 121 | var dragAnchorX = 0.0 122 | var dragAnchorY = 0.0 123 | rootGroup.onMousePressed = me => { 124 | dragAnchorX = me.screenX - stage.x.value 125 | dragAnchorY = me.screenY - stage.y.value 126 | } 127 | rootGroup.onMouseDragged = me => { 128 | stage.x = me.screenX - dragAnchorX 129 | stage.y = me.screenY - dragAnchorY 130 | } 131 | 132 | textStageX.text <== new StringProperty("x: ") + stage.x.asString 133 | textStageY.text <== new StringProperty("y: ") + stage.y.asString 134 | textStageW.text <== new StringProperty("width: ") + stage.width.asString 135 | textStageH.text <== new StringProperty("height: ") + stage.height.asString 136 | textStageF.text <== new StringProperty("focused: ") + stage.focused.asString 137 | stage.resizable = false 138 | stage.resizable <==> checkBoxResizable.selected 139 | checkBoxFullScreen.onAction = () => stage.fullScreen = checkBoxFullScreen.selected() 140 | 141 | stage.title <== titleTextField.text 142 | 143 | stage.initStyle(stageStyle) 144 | stage.onCloseRequest = () => println("Stage is closing") 145 | stage.show() 146 | 147 | val primScreenBounds = Screen.primary.visualBounds 148 | stage.x = (primScreenBounds.width - stage.width()) / 2 149 | stage.y = (primScreenBounds.height - stage.height()) / 2 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/proscalafx/ch02/zenpong/ZenPongMain.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch02.zenpong 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.scene.Scene 6 | import scalafx.scene.paint.{Color, CycleMethod, LinearGradient, Stop} 7 | 8 | import scala.language.postfixOps 9 | 10 | object ZenPongMain extends JFXApp3 { 11 | 12 | override def start(): Unit = { 13 | 14 | val zenPong = new ZenPong() 15 | 16 | stage = new PrimaryStage { 17 | title = "ZenPong Example" 18 | scene = new Scene(500, 500) { 19 | fill = LinearGradient( 20 | startX = 0.0, 21 | startY = 0.0, 22 | endX = 0.0, 23 | endY = 1.0, 24 | proportional = true, 25 | cycleMethod = CycleMethod.NoCycle, 26 | stops = List(Stop(0.0, Color.Black), Stop(0.0, Color.Gray)) 27 | ) 28 | content = zenPong.pongComponents 29 | } 30 | } 31 | 32 | zenPong.initialize() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/BidirectionalBindingExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.property.StringProperty 4 | 5 | object BidirectionalBindingExample extends App { 6 | println("Constructing two StringProperty objects.") 7 | val prop1 = new StringProperty("") 8 | val prop2 = new StringProperty("") 9 | 10 | println("Calling bindBidirectional (<==>).") 11 | prop2 <==> prop1 12 | 13 | println("prop1.isBound = " + prop1.isBound) 14 | println("prop2.isBound = " + prop2.isBound) 15 | 16 | println("Calling prop1.set(\"prop1 says: Hi!\")") 17 | prop1() = "prop1 says: Hi!" 18 | println("prop2.get returned:") 19 | println(prop2()) 20 | 21 | println("""Calling prop2.set(prop2.get + "\nprop2 says: Bye!")""") 22 | prop2() = prop2() + "\nprop2 says: Bye!" 23 | println("prop1.get returned:") 24 | println(prop1()) 25 | } 26 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/HeronsFormulaDirectExtensionExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.Includes.* 4 | import scalafx.beans.binding.Bindings 5 | import scalafx.beans.property.DoubleProperty 6 | 7 | object HeronsFormulaDirectExtensionExample extends App { 8 | val a = DoubleProperty(0) 9 | val b = DoubleProperty(0) 10 | val c = DoubleProperty(0) 11 | 12 | val area = Bindings.createDoubleBinding( 13 | () => { 14 | val a0 = a() 15 | val b0 = b() 16 | val c0 = c() 17 | 18 | if ((a0 + b0 > c0) && (b0 + c0 > a0) && (c0 + a0 > b0)) { 19 | val s = (a0 + b0 + c0) / 2.0d 20 | math.sqrt(s * (s - a0) * (s - b0) * (s - c0)) 21 | } else { 22 | 0 23 | } 24 | }, 25 | a, 26 | b, 27 | c 28 | ) 29 | 30 | // Use braces "()" to access values hold by properties, bindings, like `area`, and numeric expressions. 31 | a() = 3 32 | b() = 4 33 | c() = 5 34 | printf( 35 | "Given sides a = %1.0f, b = %1.0f, and c = %1.0f," + 36 | " the area of the triangle is %3.2f\n", 37 | a(), 38 | b(), 39 | c(), 40 | area() 41 | ) 42 | 43 | a() = 2 44 | b() = 2 45 | c() = 2 46 | printf( 47 | "Given sides a = %1.0f, b = %1.0f, and c = %1.0f," + 48 | " the area of the triangle is %3.2f\n", 49 | a(), 50 | b(), 51 | c(), 52 | area() 53 | ) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/HeronsFormulaExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.Includes.* 4 | import scalafx.beans.property.DoubleProperty 5 | 6 | object HeronsFormulaExample extends App { 7 | val a = DoubleProperty(0.0) 8 | val b = DoubleProperty(0) 9 | val c = DoubleProperty(0) 10 | 11 | val s = (a + b + c) / 2.0 12 | 13 | val areaSquared = 14 | when(((a + b) > c) && ((b + c) > a) && ((a + c) > b)) choose (s * (s - a) * (s - b) * (s - c)) otherwise 0.0 15 | 16 | a() = 3 17 | b() = 4 18 | c() = 5 19 | 20 | printf( 21 | "Given sides a = %1.0f, b = %1.0f, and c = %1.0f," + 22 | " the area of the triangle is %3.2f\n", 23 | a(), 24 | b(), 25 | c(), 26 | math.sqrt(areaSquared.get) 27 | ) 28 | 29 | a() = 2 30 | b() = 2 31 | c() = 2 32 | 33 | printf( 34 | "Given sides a = %1.0f, b = %1.0f, and c = %1.0f," + 35 | " the area of the triangle is %3.2f\n", 36 | a(), 37 | b(), 38 | c(), 39 | math.sqrt(areaSquared.get) 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/MotivatingExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.property.IntegerProperty 4 | 5 | object MotivatingExample extends App { 6 | 7 | var intProperty: IntegerProperty = _ 8 | 9 | def createProperty(): Unit = { 10 | println() 11 | intProperty = IntegerProperty(1024) 12 | println("intProperty = " + intProperty) 13 | println("intProperty.get = " + intProperty.get) 14 | println("intProperty.value = " + intProperty.value) 15 | println("intProperty() = " + intProperty()) 16 | } 17 | 18 | def addAndRemoveInvalidationListener(): Unit = { 19 | println() 20 | val subscription = intProperty.onInvalidate { 21 | observable => println("The observable has been invalidated: " + observable + ".") 22 | } 23 | 24 | println("Added invalidation listener.") 25 | 26 | println("Calling intProperty.set(2048).") 27 | intProperty() = 2048 28 | 29 | println("Calling intProperty.setValue(3072).") 30 | intProperty() = Integer.valueOf(3072) 31 | 32 | subscription.cancel() 33 | System.out.println("Removed invalidation listener.") 34 | 35 | println("Calling intProperty.set(4096).") 36 | intProperty() = 4096 37 | } 38 | 39 | def addAndRemoveChangeListener(): Unit = { 40 | println() 41 | val subscription = intProperty.onChange { 42 | (_, oldValue, newValue) => 43 | println("The observableValue has changed: oldValue = " + oldValue + ", newValue = " + newValue) 44 | } 45 | println("Added change listener.") 46 | 47 | println("Calling intProperty.set(5120).") 48 | intProperty() = 5120 49 | 50 | subscription.cancel() 51 | println("Removed change listener.") 52 | 53 | println("Calling intProperty.set(6144).") 54 | intProperty() = 6144 55 | } 56 | 57 | def bindAndUnbindOnePropertyToAnother(): Unit = { 58 | println() 59 | val otherProperty = IntegerProperty(0) 60 | println("otherProperty() = " + otherProperty()) 61 | 62 | println("Binding otherProperty to intProperty.") 63 | otherProperty <== intProperty 64 | println("otherProperty() = " + otherProperty()) 65 | 66 | println("Calling intProperty.set(7168).") 67 | intProperty() = 7168 68 | println("otherProperty.get = " + otherProperty.get) 69 | 70 | println("Unbinding otherProperty from intProperty.") 71 | otherProperty.unbind() 72 | println("otherProperty() = " + otherProperty()) 73 | 74 | println("Calling intProperty.set(8192).") 75 | intProperty() = 8192 76 | println("otherProperty() = " + otherProperty()) 77 | } 78 | 79 | createProperty() 80 | addAndRemoveInvalidationListener() 81 | addAndRemoveChangeListener() 82 | bindAndUnbindOnePropertyToAnother() 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/NumericPropertiesExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.property.{DoubleProperty, FloatProperty, IntegerProperty, LongProperty} 4 | 5 | object NumericPropertiesExample extends App { 6 | val i = new IntegerProperty(null, "i", 1024) 7 | val l = new LongProperty(null, "l", 0L) 8 | val f = new FloatProperty(null, "f", 0.0f) 9 | val d = new DoubleProperty(null, "d", 0.0) 10 | println("Constructed numerical properties i, l, f, d.") 11 | 12 | println("i = " + i()) 13 | println("l = " + l()) 14 | println("f = " + f()) 15 | println("d = " + d()) 16 | 17 | l <== i 18 | f <== l 19 | d <== f 20 | println("Bound l to i, f to l, d to f.") 21 | 22 | println("i = " + i()) 23 | println("l = " + l()) 24 | println("f = " + f()) 25 | println("d = " + d()) 26 | 27 | println("Calling i.set(2048).") 28 | i() = 2048 29 | 30 | println("i = " + i()) 31 | println("l = " + l()) 32 | println("f = " + f()) 33 | println("d = " + d()) 34 | 35 | d.unbind() 36 | f.unbind() 37 | l.unbind() 38 | println("Unbound l to i, f to l, d to f.") 39 | 40 | f <== d 41 | l <== f 42 | i <== l 43 | println("Bound f to d, l to f, i to l.") 44 | 45 | println("Calling d.set(10000000000L).") 46 | d() = 10000000000L 47 | 48 | println("d = " + d()) 49 | println("f = " + f()) 50 | println("l = " + l()) 51 | println("i = " + i()) 52 | } 53 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/RectangleAreaExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.property.DoubleProperty 4 | 5 | object RectangleAreaExample extends App { 6 | println("Constructing x with initial value of 2.0.") 7 | val x = new DoubleProperty(null, "x", 2.0) 8 | println("Constructing y with initial value of 3.0.") 9 | val y = new DoubleProperty(null, "y", 3.0) 10 | println("Creating binding area with dependencies x and y.") 11 | val area = x * y 12 | println("area = " + area()) 13 | println("Setting x to 5") 14 | x() = 5 15 | println("Setting y to 7") 16 | y() = 7 17 | println("area = " + area()) 18 | } 19 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/TriangleAreaExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.binding.NumberBinding 4 | import scalafx.beans.property.IntegerProperty 5 | 6 | object TriangleAreaExample extends App { 7 | 8 | def printResult( 9 | x1: IntegerProperty, 10 | y1: IntegerProperty, 11 | x2: IntegerProperty, 12 | y2: IntegerProperty, 13 | x3: IntegerProperty, 14 | y3: IntegerProperty, 15 | area: NumberBinding 16 | ): Unit = { 17 | println("For A(%d,%d), B(%d,%d), C(%d,%d), the area of triangle ABC is %1.1f".format( 18 | x1(), 19 | y1(), 20 | x2(), 21 | y2(), 22 | x3(), 23 | y3(), 24 | area() 25 | )) 26 | } 27 | 28 | val x1 = IntegerProperty(0) 29 | val y1 = IntegerProperty(0) 30 | val x2 = IntegerProperty(0) 31 | val y2 = IntegerProperty(0) 32 | val x3 = IntegerProperty(0) 33 | val y3 = IntegerProperty(0) 34 | 35 | val x1y2 = x1 * y2 36 | val x2y3 = x2 * y3 37 | val x3y1 = x3 * y1 38 | val x1y3 = x1 * y3 39 | val x2y1 = x2 * y1 40 | val x3y2 = x3 * y2 41 | 42 | val sum1 = x1y2 + x2y3 43 | val sum2 = sum1 + x3y1 44 | val sum3 = sum2 + x3y1 45 | val diff1 = sum3 - x1y3 46 | val diff2 = diff1 - x2y1 47 | val determinant = diff2 - x3y2 48 | val area = determinant / 2.0d 49 | 50 | x1() = 0 51 | y1() = 0 52 | x2() = 6 53 | y2() = 0 54 | x3() = 4 55 | y3() = 3 56 | 57 | printResult(x1, y1, x2, y2, x3, y3, area) 58 | 59 | x1() = 1 60 | y1() = 0 61 | x2() = 2 62 | y2() = 2 63 | x3() = 0 64 | y3() = 1 65 | 66 | printResult(x1, y1, x2, y2, x3, y3, area) 67 | } 68 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/TriangleAreaFluentExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03 2 | 3 | import scalafx.beans.binding.Bindings 4 | import scalafx.beans.property.IntegerProperty 5 | 6 | object TriangleAreaFluentExample extends App { 7 | val x1 = IntegerProperty(0) 8 | val y1 = IntegerProperty(0) 9 | val x2 = IntegerProperty(0) 10 | val y2 = IntegerProperty(0) 11 | val x3 = IntegerProperty(0) 12 | val y3 = IntegerProperty(0) 13 | 14 | val area = ((x1 * y2) + (x2 * y3) + (x3 * y1) - (x1 * y3) - (x2 * y1) - (x3 * y2)) / 2.0 15 | 16 | val output = Bindings.createStringBinding( 17 | () => 18 | f"For A(${x1()},${y1()}), B(${x2()},${y2()}), C(${x3()},${y3()}), the area of triangle ABC is ${area().doubleValue()}%.1f", 19 | x1, 20 | y1, 21 | x2, 22 | y2, 23 | x3, 24 | y3, 25 | area 26 | ) 27 | 28 | x1() = 0 29 | y1() = 0 30 | x2() = 6 31 | y2() = 0 32 | x3() = 4 33 | y3() = 3 34 | 35 | println(output()) 36 | 37 | x1() = 1 38 | y1() = 0 39 | x2() = 2 40 | y2() = 2 41 | x3() = 0 42 | y3() = 1 43 | 44 | println(output()) 45 | } 46 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/scalafxbean/ScalaFXBeanControllerExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03.scalafxbean 2 | 3 | import scalafx.scene.paint.Color 4 | 5 | class ScalaFXBeanControllerExample(model: ScalaFXBeanModelExample, view: ScalaFXBeanViewExample) { 6 | 7 | def incrementIPropertyOnModel(): Unit = { 8 | model.i() = model.i() + 1 9 | } 10 | 11 | def changeStrPropertyOnModel(): Unit = { 12 | val str = model.str() 13 | model.str() = if (str == "Hello") "World" else "Hello" 14 | } 15 | 16 | def switchColorPropertyOnModel(): Unit = { 17 | val color = model.color() 18 | model.color() = if (color == Color.Black) Color.White else Color.Black 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/scalafxbean/ScalaFXBeanMainExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03.scalafxbean 2 | 3 | object ScalaFXBeanMainExample extends App { 4 | val model = new ScalaFXBeanModelExample() 5 | val view = new ScalaFXBeanViewExample(model) 6 | val controller = new ScalaFXBeanControllerExample(model, view) 7 | 8 | controller.incrementIPropertyOnModel() 9 | controller.changeStrPropertyOnModel() 10 | controller.switchColorPropertyOnModel() 11 | controller.incrementIPropertyOnModel() 12 | controller.changeStrPropertyOnModel() 13 | controller.switchColorPropertyOnModel() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/scalafxbean/ScalaFXBeanModelExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03.scalafxbean 2 | 3 | import scalafx.beans.property.{IntegerProperty, ObjectProperty, StringProperty} 4 | import scalafx.scene.paint.Color 5 | 6 | class ScalaFXBeanModelExample { 7 | 8 | val i = new IntegerProperty(this, "i", 0) 9 | 10 | def i_=(value: Int): Unit = { 11 | i() = value 12 | } 13 | 14 | val str = new StringProperty(this, "str", "Hello") 15 | 16 | def str_=(value: String): Unit = { 17 | str() = value 18 | } 19 | 20 | val color = new ObjectProperty[Color](this, "color", Color.Black) 21 | 22 | def color_=(value: Color): Unit = { 23 | color() = value 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/proscalafx/ch03/scalafxbean/ScalaFXBeanViewExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch03.scalafxbean 2 | 3 | class ScalaFXBeanViewExample(val model: ScalaFXBeanModelExample) { 4 | 5 | model.i.onChange((_, oldValue, newValue) => { 6 | println("Property i changed: old value = " + oldValue + ", new value = " + newValue) 7 | }) 8 | 9 | model.str.onChange((_, oldValue, newValue) => { 10 | println("Property str changed: old value = " + oldValue + ", new value = " + newValue) 11 | }) 12 | 13 | model.color.onChange((_, oldValue, newValue) => { 14 | println("Property color changed: old value = " + oldValue + ", new value = " + newValue) 15 | }) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/examples/AlignUsingStackAndTile.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.examples 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.Pos 7 | import scalafx.scene.Scene 8 | import scalafx.scene.layout.{StackPane, TilePane} 9 | import scalafx.scene.paint.Color 10 | import scalafx.scene.text.{Font, FontWeight, Text} 11 | 12 | object AlignUsingStackAndTile extends JFXApp3 { 13 | 14 | override def start(): Unit = { 15 | 16 | val left = new StackPane { 17 | style = "-fx-background-color: black" 18 | children = new Text { 19 | text = "ScalaFX" 20 | font = Font.font(null, FontWeight.Bold, 18) 21 | fill = Color.White 22 | alignmentInParent = Pos.CenterRight 23 | } 24 | } 25 | 26 | stage = new PrimaryStage { 27 | scene = new Scene(400, 100) { 28 | content = new TilePane { 29 | snapToPixel = false 30 | children = List( 31 | left, 32 | new Text { 33 | text = "Reversi" 34 | font = Font.font(null, FontWeight.Bold, 18) 35 | alignmentInParent = Pos.CenterLeft 36 | } 37 | ) 38 | } 39 | } 40 | } 41 | 42 | left.prefWidth <== stage.scene().width / 2 43 | left.prefHeight <== stage.scene().height 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/examples/BorderLayoutExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.examples 2 | 3 | import proscalafx.ch04.reversi.model.{Black, Owner, ReversiModel, White} 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.geometry.Pos 8 | import scalafx.scene.effect.{DropShadow, InnerShadow} 9 | import scalafx.scene.layout.* 10 | import scalafx.scene.paint.Color 11 | import scalafx.scene.shape.Ellipse 12 | import scalafx.scene.text.{Font, FontWeight, Text} 13 | import scalafx.scene.{Node, Scene} 14 | 15 | object BorderLayoutExample extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val borderPane = new BorderPane { 20 | top = createTitle 21 | center = createBackground 22 | bottom = createScoreBoxes 23 | } 24 | 25 | stage = new PrimaryStage { 26 | scene = new Scene(600, 400) { 27 | // Assign borderPane directly to `root` to avoid layout issues. 28 | // If assigned to `content` there will be `Group` node at root that interferes with automatic rescaling. 29 | root = borderPane 30 | } 31 | } 32 | } 33 | 34 | // --------------------------------------------------------------------------- 35 | 36 | private def createTitle = new TilePane { 37 | snapToPixel = false 38 | children = List( 39 | new StackPane { 40 | style = "-fx-background-color: black" 41 | children = new Text("ScalaFX") { 42 | font = Font.font(null, FontWeight.Bold, 18) 43 | fill = Color.White 44 | alignmentInParent = Pos.CenterRight 45 | } 46 | }, 47 | new Text("Reversi") { 48 | font = Font.font(null, FontWeight.Bold, 18) 49 | alignmentInParent = Pos.CenterLeft 50 | } 51 | ) 52 | prefTileHeight = 40 53 | prefTileWidth <== parent.selectDouble("width") / 2 54 | } 55 | 56 | private def createBackground = new Region { 57 | style = "-fx-background-color: radial-gradient(radius 100%, white, gray)" 58 | } 59 | 60 | private def createScoreBoxes = new TilePane { 61 | snapToPixel = false 62 | prefColumns = 2 63 | children = List( 64 | createScore(Black), 65 | createScore(White) 66 | ) 67 | prefTileWidth <== parent.selectDouble("width") / 2 68 | } 69 | 70 | private def createScore(owner: Owner): Node = { 71 | 72 | val innerShadow = new InnerShadow() { 73 | color = Color.DodgerBlue 74 | choke = 0.5 75 | } 76 | val backgroundRegion = new Region { 77 | style = "-fx-background-color: " + owner.opposite.colorStyle 78 | if (Black == owner) { 79 | effect = innerShadow 80 | } 81 | } 82 | 83 | val dropShadow = new DropShadow() { 84 | color = Color.DodgerBlue 85 | spread = 0.2 86 | } 87 | 88 | val piece = new Ellipse { 89 | radiusX = 32 90 | radiusY = 20 91 | fill = owner.color 92 | if (Black == owner) { 93 | effect = dropShadow 94 | } 95 | } 96 | 97 | val score = new Text { 98 | font = Font.font(null, FontWeight.Bold, 100) 99 | fill = owner.color 100 | text <== ReversiModel.score(owner).asString 101 | } 102 | 103 | val remaining = new Text { 104 | font = Font.font(null, FontWeight.Bold, 12) 105 | fill = owner.color 106 | text <== ReversiModel.turnsRemaining(owner).asString() + " turns remaining" 107 | } 108 | 109 | new StackPane { 110 | children = List( 111 | backgroundRegion, 112 | new FlowPane { 113 | hgap = 20 114 | vgap = 10 115 | alignment = Pos.Center 116 | children = List( 117 | score, 118 | new VBox { 119 | alignment = Pos.Center 120 | spacing = 10 121 | children = List( 122 | piece, 123 | remaining 124 | ) 125 | } 126 | ) 127 | } 128 | ) 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/examples/CenterUsingBind.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.examples 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.VPos 7 | import scalafx.scene.Scene 8 | import scalafx.scene.text.{Font, FontWeight, Text} 9 | 10 | object CenterUsingBind extends JFXApp3 { 11 | 12 | override def start(): Unit = { 13 | 14 | val text = new Text("ScalaFX Reversi") { 15 | textOrigin = VPos.Top 16 | font = Font.font(null, FontWeight.Bold, 18) 17 | } 18 | 19 | stage = new PrimaryStage { 20 | scene = new Scene(400, 100) { 21 | content = text 22 | } 23 | } 24 | 25 | text.layoutX <== (stage.scene().width - text.prefWidth(-1)) / 2 26 | text.layoutY <== (stage.scene().height - text.prefHeight(-1)) / 2 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/examples/CenterUsingStack.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.examples 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.scene.Scene 7 | import scalafx.scene.layout.StackPane 8 | import scalafx.scene.paint.Color 9 | import scalafx.scene.shape.Ellipse 10 | import scalafx.scene.text.{Font, FontWeight, Text} 11 | 12 | object CenterUsingStack extends JFXApp3 { 13 | 14 | override def start(): Unit = { 15 | 16 | val ellipse = new Ellipse 17 | 18 | stage = new PrimaryStage { 19 | scene = new Scene(400, 100) { 20 | content = new StackPane { 21 | children = List( 22 | ellipse, 23 | new Text("ScalaFX Reversi") { 24 | font = Font.font(null, FontWeight.Bold, 18) 25 | fill = Color.White 26 | } 27 | ) 28 | } 29 | } 30 | } 31 | 32 | ellipse.radiusX <== stage.scene().width / 2 33 | ellipse.radiusY <== stage.scene().height / 2 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/examples/PlayerScoreExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.examples 2 | 3 | import proscalafx.ch04.reversi.model.{Black, Owner, ReversiModel, White} 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.geometry.Pos 8 | import scalafx.scene.Scene 9 | import scalafx.scene.effect.{DropShadow, InnerShadow} 10 | import scalafx.scene.layout.* 11 | import scalafx.scene.paint.Color 12 | import scalafx.scene.shape.Ellipse 13 | import scalafx.scene.text.{Font, FontWeight, Text} 14 | 15 | object PlayerScoreExample extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val tiles = new TilePane() { 20 | snapToPixel = false 21 | children = List( 22 | createScore(Black), 23 | createScore(White) 24 | ) 25 | } 26 | 27 | stage = new PrimaryStage() { 28 | scene = new Scene(600, 140) { 29 | content = tiles 30 | } 31 | } 32 | 33 | tiles.prefTileWidth <== stage.scene().width / 2 34 | tiles.prefTileHeight <== stage.scene().height 35 | } 36 | 37 | // --------------------------------------------------------------------------- 38 | 39 | private def createScore(owner: Owner): StackPane = { 40 | 41 | val innerShadow = new InnerShadow() { 42 | color = Color.DodgerBlue 43 | choke = 0.5 44 | } 45 | 46 | val backgroundRegion = new Region() { 47 | style = "-fx-background-color: " + owner.opposite.colorStyle 48 | if (Black == owner) { 49 | effect = innerShadow 50 | } 51 | } 52 | 53 | val dropShadow = new DropShadow() { 54 | color = Color.DodgerBlue 55 | spread = 0.2 56 | } 57 | 58 | val piece = new Ellipse() { 59 | radiusX = 32 60 | radiusY = 20 61 | fill = owner.color 62 | if (Black == owner) { 63 | effect = dropShadow 64 | } 65 | } 66 | 67 | val score = new Text() { 68 | font = Font.font(null, FontWeight.Bold, 100) 69 | fill = owner.color 70 | text <== ReversiModel.score(owner).asString() 71 | } 72 | 73 | val remaining = new Text() { 74 | font = Font.font(null, FontWeight.Bold, 12) 75 | fill = owner.color 76 | text <== ReversiModel.turnsRemaining(owner).asString() + " turns remaining" 77 | } 78 | 79 | new StackPane() { 80 | children = List( 81 | backgroundRegion, 82 | new FlowPane() { 83 | hgap = 20 84 | vgap = 10 85 | alignment = Pos.Center 86 | children = List( 87 | score, 88 | new VBox() { 89 | alignment = Pos.Center 90 | spacing = 10 91 | children = List( 92 | piece, 93 | remaining 94 | ) 95 | } 96 | ) 97 | } 98 | ) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/model/Owner.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.model 2 | 3 | import scalafx.scene.paint.Color 4 | 5 | sealed case class Owner(color: Color, colorStyle: String) { 6 | 7 | def opposite: Owner = this match { 8 | case White => Black 9 | case Black => White 10 | case _ => NONE 11 | } 12 | 13 | } 14 | 15 | object NONE extends Owner(Color.Transparent, "") 16 | 17 | object White extends Owner(Color.White, "white") 18 | 19 | object Black extends Owner(Color.Black, "black") 20 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/model/ReversiModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.model 2 | 3 | import scalafx.Includes.{when, *} 4 | import scalafx.beans.Observable 5 | import scalafx.beans.binding.* 6 | import scalafx.beans.property.ObjectProperty 7 | 8 | import scala.collection.mutable.ListBuffer 9 | 10 | object ReversiModel { 11 | 12 | val BOARD_SIZE = 8 13 | 14 | val turn = ObjectProperty[Owner](Black) 15 | 16 | val board = Array.tabulate(BOARD_SIZE, BOARD_SIZE)((_, _) => ObjectProperty[Owner](NONE)) 17 | 18 | initBoard() 19 | 20 | private def initBoard(): Unit = { 21 | val center1 = BOARD_SIZE / 2 - 1 22 | val center2 = BOARD_SIZE / 2 23 | board(center1)(center1)() = White 24 | board(center1)(center2)() = Black 25 | board(center2)(center1)() = Black 26 | board(center2)(center2)() = White 27 | } 28 | 29 | def restart(): Unit = { 30 | board.flatten.foreach(_() = NONE) 31 | 32 | initBoard() 33 | turn() = Black 34 | } 35 | 36 | def score(owner: Owner): NumberExpression = { 37 | board.flatten.map(p => when(p === owner) choose 1 otherwise 0).reduce(_ + _) 38 | } 39 | 40 | def turnsRemaining(owner: Owner): NumberBinding = { 41 | val emptyCellCount = score(NONE) 42 | 43 | when(turn === owner) choose ((emptyCellCount + 1) / 2) otherwise (emptyCellCount / 2) 44 | } 45 | 46 | def legalMove(x: Int, y: Int): BooleanBinding = { 47 | (board(x)(y) === NONE) && ( 48 | canFlip(x, y, 0, -1, turn) || 49 | canFlip(x, y, -1, -1, turn) || 50 | canFlip(x, y, -1, 0, turn) || 51 | canFlip(x, y, -1, 1, turn) || 52 | canFlip(x, y, 0, 1, turn) || 53 | canFlip(x, y, 1, 1, turn) || 54 | canFlip(x, y, 1, 0, turn) || 55 | canFlip(x, y, 1, -1, turn) 56 | ) 57 | } 58 | 59 | private def canFlip( 60 | cellX: Int, 61 | cellY: Int, 62 | directionX: Int, 63 | directionY: Int, 64 | turn: ObjectProperty[Owner] 65 | ): BooleanBinding = { 66 | // Build list of dependencies for binding 67 | val dependencies = ListBuffer.empty[Observable] 68 | var x = cellX + directionX 69 | var y = cellY + directionY 70 | while (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE) { 71 | dependencies += board(x)(y) 72 | x += directionX 73 | y += directionY 74 | } 75 | dependencies += turn 76 | 77 | Bindings.createBooleanBinding( 78 | // Flip evaluation 79 | () => { 80 | val turnVal = turn.get 81 | var x = cellX + directionX 82 | var y = cellY + directionY 83 | var first = true 84 | var result: Option[Boolean] = None 85 | while ( 86 | result.isEmpty && 87 | x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && board(x)(y).get != NONE 88 | ) { 89 | if (board(x)(y).get == turnVal) { 90 | result = Option(!first) 91 | } 92 | first = false 93 | x += directionX 94 | y += directionY 95 | } 96 | result.getOrElse(false) 97 | }, 98 | dependencies.toSeq* 99 | ) 100 | } 101 | 102 | def play(cellX: Int, cellY: Int): Unit = { 103 | if (legalMove(cellX, cellY).get) { 104 | board(cellX)(cellY)() = turn() 105 | flip(cellX, cellY, 0, -1, turn) 106 | flip(cellX, cellY, -1, -1, turn) 107 | flip(cellX, cellY, -1, 0, turn) 108 | flip(cellX, cellY, -1, 1, turn) 109 | flip(cellX, cellY, 0, 1, turn) 110 | flip(cellX, cellY, 1, 1, turn) 111 | flip(cellX, cellY, 1, 0, turn) 112 | flip(cellX, cellY, 1, -1, turn) 113 | turn.value = turn.value.opposite 114 | } 115 | } 116 | 117 | def flip(cellX: Int, cellY: Int, directionX: Int, directionY: Int, turn: ObjectProperty[Owner]): Unit = { 118 | if (canFlip(cellX, cellY, directionX, directionY, turn).get) { 119 | var x = cellX + directionX 120 | var y = cellY + directionY 121 | while (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && board(x)(y)() != turn()) { 122 | board(x)(y)() = turn() 123 | x += directionX 124 | y += directionY 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/reversipieces/ReversiPieceTest.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.reversipieces 2 | 3 | import proscalafx.ch04.reversi.model.{Black, White} 4 | import proscalafx.ch04.reversi.ui.{ReversiPiece, ReversiSquare} 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.scene.Scene 8 | import scalafx.scene.layout.{HBox, Priority, StackPane} 9 | 10 | object ReversiPieceTest extends JFXApp3 { 11 | 12 | override def start(): Unit = { 13 | 14 | // Unlike in many other examples, here content of the scene is assigned using `root` rather 15 | // than `content` properties. 16 | // `scalafx.scene.Scene` creates by default its root element as `Group`. If we assign using `content`, children 17 | // are added to that group. That may have undesired layout complications. In our case it will prevent automatic 18 | // resizing of the content. 19 | // To work around this, we assign to `root` directly. 20 | stage = new PrimaryStage() { 21 | scene = new Scene() { 22 | root = new HBox { 23 | snapToPixel = false 24 | children = List( 25 | new StackPane { 26 | children = List( 27 | new ReversiSquare(0, 0), 28 | new ReversiPiece(White) 29 | ) 30 | hgrow = Priority.Always 31 | }, 32 | new StackPane { 33 | children = List( 34 | new ReversiSquare(0, 0), 35 | new ReversiPiece(Black) 36 | ) 37 | hgrow = Priority.Always 38 | } 39 | ) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/reversipieces/ReversiSquareTest.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.reversipieces 2 | 3 | import proscalafx.ch04.reversi.ui.ReversiSquare 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.scene.Scene 7 | import scalafx.scene.layout.StackPane 8 | 9 | object ReversiSquareTest extends JFXApp3 { 10 | 11 | override def start(): Unit = { 12 | // Unlike in many other examples, here content of the scene is assigned using `root` rather 13 | // than `content` properties. 14 | // `scalafx.scene.Scene` creates by default its root element as `Group`. If we assign using `content`, children 15 | // are added to that group. That may have undesired layout complications. In our case it will prevent automatic 16 | // resizing of the content. 17 | // To work around this, we assign to `root` directly. 18 | stage = new PrimaryStage { 19 | scene = new Scene() { 20 | root = new StackPane { 21 | children = new ReversiSquare(0, 0) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/ui/ReversiApp.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.ui 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.scene.Scene 6 | import scalafx.scene.layout.* 7 | 8 | object ReversiApp extends JFXApp3 { 9 | 10 | override def start(): Unit = { 11 | 12 | val reversi = new Reversi() 13 | 14 | stage = new PrimaryStage() { 15 | scene = new Scene(400, 600) { 16 | root = new AnchorPane() { 17 | children = List( 18 | reversi.game, 19 | reversi.restart 20 | ) 21 | } 22 | } 23 | } 24 | 25 | AnchorPane.setTopAnchor(reversi.game, 0d) 26 | AnchorPane.setBottomAnchor(reversi.game, 0d) 27 | AnchorPane.setLeftAnchor(reversi.game, 0d) 28 | AnchorPane.setRightAnchor(reversi.game, 0d) 29 | AnchorPane.setRightAnchor(reversi.restart, 10d) 30 | AnchorPane.setTopAnchor(reversi.restart, 10d) 31 | 32 | // Code commended below was only used in the first edition of the book 33 | // if (Platform.isSupported(ConditionalFeature.Scene3D)) { 34 | // stage.scene().camera = new PerspectiveCamera() { 35 | // fieldOfView = 60 36 | // }.delegate 37 | // } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/ui/ReversiPiece.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.ui 2 | 3 | import proscalafx.ch04.reversi.model.{NONE, Owner, White} 4 | import scalafx.Includes.* 5 | import scalafx.beans.property.ObjectProperty 6 | import scalafx.scene.effect.Reflection 7 | import scalafx.scene.layout.Region 8 | 9 | object ReversiPiece { 10 | 11 | private val nonePieceStyle = "radius: 0; " 12 | private val whitePieceStyle = "-fx-background-color: radial-gradient(radius 100%, white .4, gray .9, darkgray 1); " 13 | private val blackPieceStyle = "-fx-background-color: radial-gradient(radius 100%, white .0, black .6); " 14 | private val tileStyle = "-fx-background-radius: 1000em; -fx-background-insets: 5" 15 | 16 | } 17 | 18 | class ReversiPiece(_owner: Owner = NONE) extends Region { 19 | 20 | import ReversiPiece.* 21 | 22 | val owner = new ObjectProperty[Owner](this, "owner", _owner) 23 | 24 | def owner_=(value: Owner): Unit = { 25 | owner() = value 26 | } 27 | 28 | style <== ( 29 | when(owner === NONE) choose nonePieceStyle otherwise 30 | (when(owner === White) choose whitePieceStyle otherwise blackPieceStyle) 31 | ) + tileStyle 32 | 33 | effect = new Reflection { 34 | fraction = 1.0 35 | topOffset <== height * (-0.75) 36 | } 37 | 38 | prefWidth = 180 39 | prefHeight = 180 40 | mouseTransparent = true 41 | } 42 | -------------------------------------------------------------------------------- /src/proscalafx/ch04/reversi/ui/ReversiSquare.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch04.reversi.ui 2 | 3 | import javafx.scene.layout as jfxsl 4 | import proscalafx.ch04.reversi.model.ReversiModel 5 | import scalafx.Includes.* 6 | import scalafx.animation.FadeTransition 7 | import scalafx.geometry.{HPos, VPos} 8 | import scalafx.scene.effect.{Light, Lighting} 9 | import scalafx.scene.layout.Region 10 | import scalafx.util.Duration 11 | 12 | class ReversiSquare(val x: Int, val y: Int) extends Region { 13 | 14 | private val highlight = new Region { 15 | opacity = 0 16 | style = "-fx-border-width: 3; -fx-border-color: dodgerblue" 17 | } 18 | 19 | override val delegate: jfxsl.Region = new jfxsl.Region { 20 | getChildren.add(highlight) 21 | 22 | protected override def layoutChildren(): Unit = { 23 | layoutInArea(highlight, 0, 0, getWidth, getHeight, getBaselineOffset, HPos.Center, VPos.Center) 24 | } 25 | } 26 | 27 | private val highlightTransition = new FadeTransition { 28 | node = highlight 29 | duration = Duration(200) 30 | fromValue = 0 31 | toValue = 1 32 | } 33 | 34 | style <== when(ReversiModel.legalMove(x, y)) choose 35 | "-fx-background-color: derive(dodgerblue, -60%)" otherwise 36 | "-fx-background-color: burlywood" 37 | 38 | effect = new Lighting { 39 | light = new Light.Distant { 40 | azimuth = -135 41 | elevation = 30 42 | } 43 | } 44 | 45 | prefHeight = 200 46 | prefWidth = 200 47 | 48 | onMouseEntered = () => { 49 | if (ReversiModel.legalMove(x, y).get) { 50 | highlightTransition.rate() = 1 51 | highlightTransition.play() 52 | } 53 | } 54 | 55 | onMouseExited = () => { 56 | highlightTransition.rate = -1 57 | highlightTransition.play() 58 | } 59 | 60 | onMouseClicked = () => { 61 | ReversiModel.play(x, y) 62 | highlightTransition.rate() = -1 63 | highlightTransition.play() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch05/model/Person.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch05.model 2 | 3 | import scalafx.beans.property.StringProperty 4 | 5 | /** 6 | * @author Jarek Sacha 7 | */ 8 | class Person(firstName_ : String, lastName_ : String, phone_ : String) { 9 | 10 | val firstName = new StringProperty(this, "firstName", firstName_) 11 | val lastName = new StringProperty(this, "lastName", lastName_) 12 | val phone = new StringProperty(this, "phone", phone_) 13 | 14 | override def toString: String = { 15 | "Person: " + firstName() + " " + lastName() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/proscalafx/ch05/model/StarterAppModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch05.model 2 | 3 | import scalafx.beans.property.DoubleProperty 4 | import scalafx.collections.ObservableBuffer 5 | 6 | import scala.util.Random 7 | 8 | /** 9 | * @author Jarek Sacha 10 | */ 11 | class StarterAppModel { 12 | 13 | val listViewItems = new ObservableBuffer[String]() 14 | val choiceBoxItems = ObservableBuffer("Choice A", "Choice B", "Choice C", "Choice D") 15 | val maxRpm: Double = 8000d 16 | val rpm = new DoubleProperty(this, "rpm", 0) 17 | val maxKph: Double = 300.0 18 | val kph = new DoubleProperty(this, "kph", 0) 19 | 20 | def getTeamMembers: ObservableBuffer[Person] = { 21 | 22 | val teamMembers = new ObservableBuffer[Person]() 23 | 24 | for (i <- 1 to 1000) { 25 | teamMembers += new Person("FirstName" + i, "LastName" + i, "Phone" + i) 26 | } 27 | 28 | teamMembers 29 | } 30 | 31 | def randomWebSite(): String = { 32 | 33 | val webSites: Array[String] = Array( 34 | "https://www.jfx-central.com/", 35 | "https://openjfx.io/", 36 | "https://github.com/mhrimaz/AwesomeJavaFX", 37 | "http://fxexperience.com", 38 | // "http://steveonjava.com", 39 | // "https://javafxpert.com", 40 | "https://pleasingsoftware.blogspot.com", 41 | "https://www.weiqigao.com/blog", 42 | // "https://edencoding.com/category/javafx/", 43 | "https://codingonthestaircase.wordpress.com/", 44 | "https://www.google.com/" 45 | ) 46 | 47 | val randomIdx = new Random().nextInt(webSites.length) 48 | webSites(randomIdx) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/proscalafx/ch05/ui/images/paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch05/ui/images/paper.png -------------------------------------------------------------------------------- /src/proscalafx/ch05/ui/starterApp.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011, Pro JavaFX Authors 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 1. Redistributions of source code must retain the above copyright notice, 8 | * this list of conditions and the following disclaimer. 9 | * 2. Redistributions in binary form must reproduce the above copyright notice, 10 | * this list of conditions and the following disclaimer in the documentation 11 | * and/or other materials provided with the distribution. 12 | * 3. Neither the name of JFXtras nor the names of its contributors may be used 13 | * to endorse or promote products derived from this software without 14 | * specific prior written permission. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | * POSSIBILITY OF SUCH DAMAGE. 27 | * 28 | * starterApp.css - A CSS (Cascading Style Sheet) file the is referred to by 29 | * the StarterAppMain.java program to apply styles to the scene and its nodes. 30 | * Note: Syntax recommendation http://www.w3.org/TR/REC-CSS2/ 31 | * 32 | * Developed 2011 by James L. Weaver jim.weaver [at] javafxpert.com 33 | * as a JavaFX SDK 2.0 example for the Pro JavaFX book. 34 | */ 35 | 36 | #newButton { 37 | -fx-padding: 4 4 4 4; 38 | } 39 | 40 | #editButton { 41 | -fx-padding: 4 4 4 4; 42 | } 43 | 44 | #deleteButton { 45 | -fx-padding: 4 4 4 4; 46 | } 47 | 48 | #boldButton { 49 | -fx-padding: 4 4 4 4; 50 | } 51 | 52 | #italicButton { 53 | -fx-padding: 4 4 4 4; 54 | } 55 | 56 | #leftAlignButton { 57 | -fx-padding: 4 4 4 4; 58 | } 59 | 60 | #centerAlignButton { 61 | -fx-padding: 4 4 4 4; 62 | } 63 | 64 | #rightAlignButton { 65 | -fx-padding: 4 4 4 4; 66 | } 67 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/BufferChangeEventExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.collections.ObservableBuffer 4 | import scalafx.collections.ObservableBuffer.* 5 | 6 | /** 7 | * Example of processing of "change" notifications from ScalaFX `ObservableBuffer`, wrapper for JavaFX `ObservableList`. 8 | * 9 | * This example corresponds to JavaFX example `ListChangeEventExample`. 10 | * 11 | * ScalaFX is using a different way of passing information about modification to `ObservableBuffer`. 12 | * Each modification is represented by a [[scalafx.collections.ObservableBuffer.Change]] object. 13 | */ 14 | object BufferChangeEventExample extends App { 15 | 16 | val strings = new ObservableBuffer[String] 17 | strings.onChange((buffer, changes) => { 18 | println("\tbuffer = " + buffer.mkString("[", ", ", "]")) 19 | val log = changes 20 | .zipWithIndex 21 | .foldLeft("\tChange event data:\n")({ case (string, (change, index)) => string + prettyPrint(index, change) }) 22 | println(log) 23 | }) 24 | 25 | println("""Calling strings ++= Seq("Zero", "One", "Two", "Three"): """) 26 | strings ++= Seq("Zero", "One", "Two", "Three") 27 | 28 | println("Calling strings.sort: ") 29 | strings.sort() 30 | 31 | println("""Calling strings(1) = "Three_1": """) 32 | strings(1) = "Three_1" 33 | 34 | println("""Calling strings.setAll("One_1", "Three_1", "Two_1", "Zero_1"): """) 35 | strings.setAll("One_1", "Three_1", "Two_1", "Zero_1") 36 | 37 | println("""Calling strings --= Seq("One_1", "Two_1", "Zero_1"): """) 38 | strings --= Seq("One_1", "Two_1", "Zero_1") 39 | 40 | private def prettyPrint(index: Int, change: Change[String]): String = { 41 | val sb = new StringBuffer("\t\tcursor = " + index + "\n") 42 | sb.append("\t\tKind of change: ") 43 | 44 | change match { 45 | case Add(position, added) => 46 | sb.append("added\n") 47 | sb.append("\t\tPosition: " + position + "\n") 48 | sb.append("\t\tElement : " + added + "\n") 49 | case Remove(position, removed) => 50 | sb.append("removed\n") 51 | sb.append("\t\tPosition: " + position + "\n") 52 | sb.append("\t\tElement : " + removed + "\n") 53 | case Reorder(start, end, permutation) => 54 | sb.append("reordered\n") 55 | sb.append("\t\tAffected Range: [%d, %d]\n".format(start, end)) 56 | val strLog = (start until end) 57 | .map(i => "%d->%s".format(i, permutation(i))) 58 | .mkString("\t\tPermutation: [", ", ", "]\n") 59 | sb.append(strLog) 60 | case Update(from, to) => 61 | sb.append("updated\n") 62 | sb.append("\t\tfrom: " + from + "\n") 63 | sb.append("\t\tto : " + to + "\n") 64 | } 65 | 66 | sb.toString 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/FXCollectionsExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import javafx.collections as jfxc 4 | import scalafx.collections.ObservableBuffer 5 | import scalafx.collections.ObservableBuffer.* 6 | 7 | import java.util as ju 8 | import scala.jdk.CollectionConverters.* 9 | 10 | /** 11 | * ScalaFX version of `FXCollectionsExample` from "Pro JavaFX 2" book. 12 | * 13 | * ScalaFX does not have a direct equivalent of JavaFX `FXCollections`. 14 | * Some of static methods from `FXCollections` are members of relevant observable collection in ScalaFX, 15 | * for instance `replaceAll`. Some are in companion objects, for instance, `shuffle`. 16 | * Some are not implemented and require use of `FXCollections`. 17 | * 18 | * @author Jarek Sacha 19 | */ 20 | object FXCollectionsExample extends App { 21 | 22 | // `println` statements show JavaFX API, for easier comparison to ScalaFX API used in the code. 23 | 24 | val strings = new ObservableBuffer[String]() 25 | strings.onChange((source, changes) => { 26 | println("\tlist = " + source.mkString("[", ", ", "]")) 27 | changes.foreach(prettyPrint) 28 | println() 29 | }) 30 | 31 | println("""Calling addAll("Zero", "One", "Two", "Three"): """) 32 | strings.addAll("Zero", "One", "Two", "Three") 33 | 34 | println("Calling copy: ") 35 | jfxc.FXCollections.copy(strings, List("Four", "Five").asJava) 36 | 37 | println("Calling replaceAll: ") 38 | strings.replaceAll("Two", "Two_1") 39 | 40 | println("Calling reverse: ") 41 | ObservableBuffer.revertBuffer(strings) 42 | 43 | println("Calling rotate(strings, 2: ") 44 | jfxc.FXCollections.rotate(strings.delegate, 2) 45 | 46 | println("Calling shuffle(strings): ") 47 | ObservableBuffer.shuffle(strings) 48 | 49 | println("Calling shuffle(strings, new Random(0L)): ") 50 | ObservableBuffer.shuffle(strings, new ju.Random(0L)) 51 | 52 | println("Calling sort(strings): ") 53 | strings.sort() 54 | 55 | println("Calling sort(strings, c) with custom comparator: ") 56 | strings.sort((lhs, rhs) => lhs > rhs) 57 | 58 | println("""Calling fill(strings, "Ten"): """) 59 | ObservableBuffer.fillAll(strings, "Ten") 60 | 61 | def prettyPrint(change: Change[String]): Unit = { 62 | change match { 63 | case Add(_, added) => 64 | println("\t\tKind of change: added") 65 | println("\t\tAdded size : " + added.size) 66 | println("\t\tAdded sublist : " + mkString(added)) 67 | case Remove(_, removed) => 68 | println("\t\tKind of change: removed") 69 | println("\t\tRemoved size : " + removed.size) 70 | println("\t\tRemoved : " + mkString(removed)) 71 | case Reorder(start, end, permutation) => 72 | val permutations = for (i <- start until end) yield s"$i -> ${permutation(i)}" 73 | println("\t\tKind of change: permuted") 74 | println("\t\tPermutation : " + mkString(permutations)) 75 | case Update(from, to) => 76 | println("\t\tKind of change: updated\n") 77 | println("\t\t\tfrom: " + from + "\n") 78 | println("\t\t\tto : " + to + "\n") 79 | 80 | } 81 | } 82 | 83 | def mkString[T](seq: IterableOnce[T]): String = 84 | if (seq.iterator.isEmpty) "[]" else seq.iterator.mkString("[", ", ", "]") 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/JavaFXSceneInSwingExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import javafx.embed.swing as jfxes 4 | import scalafx.application.Platform 5 | import scalafx.beans.property.ObjectProperty 6 | import scalafx.scene.Scene 7 | import scalafx.scene.layout.VBox 8 | import scalafx.scene.paint.Color 9 | import scalafx.scene.shape.Rectangle 10 | 11 | import java.awt.event.ActionEvent 12 | import java.awt.{BorderLayout, Dimension, FlowLayout} 13 | import javax.swing.{JButton, JFrame, JPanel, WindowConstants} 14 | 15 | /** 16 | * @author Jarek Sacha 17 | */ 18 | object JavaFXSceneInSwingExample extends App { 19 | 20 | private val model = new Model() 21 | new Controller(model, new View(model)).mainLoop() 22 | 23 | private class Model { 24 | // `fill` and `stroke` are created using ObjectProperty factory method to ensure proper type parameter 25 | // to ObjectProperty. We use here, implicitly, JavaFX Paint as the type for `ObjectProperty`. 26 | val fill = ObjectProperty(this, "fillPaint", Color.LightGray) 27 | val stroke = ObjectProperty(this, "strokePaint", Color.DarkGray) 28 | } 29 | 30 | private class View(model: Model) { 31 | val frame = new JFrame("ScalaFX in Swing Example") 32 | // NOTE: ScalaFX does not currently implement JFXPanel, so we use JavaFX/Swing directly. 33 | val canvas = new jfxes.JFXPanel() { 34 | setPreferredSize(new Dimension(210, 210)) 35 | } 36 | // Execute JavaFX code on JavaFX Application Thread. 37 | Platform.runLater { 38 | val rectangle = new Rectangle { 39 | width = 200 40 | height = 200 41 | strokeWidth = 10 42 | fill <== model.fill 43 | stroke <== model.stroke 44 | } 45 | 46 | canvas.setScene(new Scene { 47 | root = new VBox { 48 | children = List(rectangle) 49 | } 50 | }.delegate) 51 | } 52 | 53 | val canvasPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10) 54 | val canvasPanel = new JPanel(canvasPanelLayout) 55 | canvasPanel.add(canvas) 56 | val changeFillButton = new JButton("Change Fill") 57 | val changeStrokeButton = new JButton("Change Stroke") 58 | val buttonPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10) 59 | val buttonPanel = new JPanel(buttonPanelLayout) 60 | buttonPanel.add(changeFillButton) 61 | buttonPanel.add(changeStrokeButton) 62 | frame.add(canvasPanel, BorderLayout.CENTER) 63 | frame.add(buttonPanel, BorderLayout.SOUTH) 64 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) 65 | frame.setLocationByPlatform(true) 66 | frame.pack() 67 | } 68 | 69 | private class Controller(model: Model, view: View) { 70 | view.changeFillButton.addActionListener((_: ActionEvent) => { 71 | Platform.runLater { 72 | model.fill() = if (model.fill() == Color.LightGray) Color.Gray else Color.LightGray 73 | } 74 | }) 75 | view.changeStrokeButton.addActionListener((_: ActionEvent) => { 76 | Platform.runLater { 77 | model.stroke() = if (model.stroke() == Color.DarkGray) Color.Black else Color.DarkGray 78 | } 79 | }) 80 | 81 | def mainLoop(): Unit = { 82 | view.frame.setVisible(true) 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/JavaFXThreadsExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.collections.ObservableBuffer 7 | import scalafx.geometry.Insets 8 | import scalafx.scene.Scene 9 | import scalafx.scene.control.{Button, ListView, TextArea} 10 | import scalafx.scene.layout.VBox 11 | 12 | import scala.jdk.CollectionConverters.* 13 | 14 | /** 15 | * ScalaFX version of `JavaFXThreadsExample` from "Pro JavaFX 2" book. 16 | * 17 | * @author Jarek Sacha 18 | */ 19 | object JavaFXThreadsExample extends JFXApp3 { 20 | 21 | class Model { 22 | val threadNames = new ObservableBuffer[String]() 23 | val stackTraces = new ObservableBuffer[String]() 24 | update() 25 | 26 | def update(): Unit = { 27 | threadNames.clear() 28 | stackTraces.clear() 29 | val map = Thread.getAllStackTraces.asScala 30 | for ((k, v) <- map) { 31 | threadNames += "\"" + k.getName + "\"" 32 | stackTraces += formatStackTrace(v) 33 | } 34 | } 35 | 36 | private def formatStackTrace(v: Array[StackTraceElement]): String = { 37 | val sb = new StringBuilder("StackTrace: \n") 38 | for (stackTraceElement <- v) { 39 | sb ++= " at " ++= stackTraceElement.toString ++= "\n" 40 | } 41 | sb.toString() 42 | } 43 | } 44 | 45 | class View(model: Model) { 46 | val threadNames = new ListView(model.threadNames) 47 | val stackTrace = new TextArea() 48 | val updateButton = new Button("Update") 49 | val scene = new Scene(440, 640) { 50 | root = new VBox { 51 | spacing = 10 52 | padding = Insets(10) 53 | children = List( 54 | threadNames, 55 | stackTrace, 56 | updateButton 57 | ) 58 | } 59 | } 60 | } 61 | 62 | override def start(): Unit = { 63 | 64 | val model = new Model() 65 | val view = new View(model) 66 | 67 | hookupEvents() 68 | stage = new PrimaryStage { 69 | title = "JavaFX Threads Information" 70 | scene = view.scene 71 | } 72 | 73 | def hookupEvents(): Unit = { 74 | view.updateButton.onAction = () => model.update() 75 | view.threadNames.selectionModel().selectedItem.onChange { 76 | val index = view.threadNames.selectionModel().getSelectedIndex 77 | if (index >= 0) { 78 | view.stackTrace.text = model.stackTraces(index) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/MapChangeEventExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.collections.ObservableMap 4 | import scalafx.collections.ObservableMap.* 5 | 6 | /** 7 | * Example of processing of "change" notifications from ScalaFX `ObservableMap`, wrapper for JavaFX `ObservableMap`. 8 | * 9 | * ScalaFX used a different way of passing information about modification to `ObservableMap`. 10 | * Each modification is represented by a [[scalafx.collections.ObservableMap.Change]] object. 11 | */ 12 | object MapChangeEventExample extends App { 13 | 14 | val map = ObservableMap.empty[String, Int] 15 | map.onChange((map, change) => { 16 | println("\tmap = " + map.mkString("[", ", ", "]")) 17 | println(prettyChange(change)) 18 | }) 19 | 20 | println("""Calling map("First") = 1: """) 21 | map("First") = 1 22 | 23 | println("""Calling map(First") = 100: """) 24 | map("First") = 100 25 | 26 | val anotherMap = Map("Second" -> 2, "Third" -> 3) 27 | println("""Calling map ++= anotherMap: """) 28 | map ++= anotherMap 29 | 30 | println("""Removing by key: Calling map -= "Second"""") 31 | map -= "Second" 32 | 33 | println("Removing by value: Calling map retain({case (k, v) => v != 3})") 34 | map filterInPlace { case (_, v) => v != 3 } 35 | 36 | def prettyChange(change: Change[_, _]): String = { 37 | val sb = new StringBuffer("\tChange event data:\n") 38 | 39 | change match { 40 | case Add(key, added) => 41 | sb.append("\t\tWas added\n") 42 | sb.append("\t\tKey : %s\n".format(key)) 43 | sb.append("\t\tValue added : %s\n".format(added)) 44 | case Remove(key, removed) => 45 | sb.append("\t\tWas removed\n") 46 | sb.append("\t\tKey : %s\n".format(key)) 47 | sb.append("\t\tValue removed: %s\n".format(removed)) 48 | case Replace(key, added, removed) => 49 | sb.append("\t\tWas replaced\n") 50 | sb.append("\t\tKey : %s\n".format(key)) 51 | sb.append("\t\tValue added : %s\n".format(added)) 52 | sb.append("\t\tValue removed: %s\n".format(removed)) 53 | } 54 | 55 | sb.toString 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/ObservableBufferExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.collections.ObservableBuffer 4 | 5 | /** 6 | * This example corresponds to JavaFX example `ObservableListExample`. 7 | * 8 | * In ScalaFX `ObservableBuffer` is a wrapper for JavaFX `ObservableList`. 9 | */ 10 | object ObservableBufferExample extends App { 11 | 12 | val strings = new ObservableBuffer[String]() 13 | strings.onInvalidate(println("\tlist invalidated")) 14 | strings.onChange((source, change) => println("\tstrings = " + source.mkString("[", ", ", "]"))) 15 | 16 | println("""Calling += "First": """) 17 | strings += "First" 18 | 19 | println("""Calling insert(0, "Zeroth"): """) 20 | strings.insert(0, "Zeroth") 21 | 22 | println(""""Calling ++= Seq("Second", "Third"): """) 23 | strings ++= Seq("Second", "Third") 24 | 25 | println("""Calling (1) = "New First"): """) 26 | strings(1) = "New First" 27 | 28 | val list = List("Second_1", "Second_2") 29 | println("Calling addAll(3, list): ") 30 | strings.insertAll(3, list) 31 | 32 | // There is a difference between meaning of the second argument of `remove` in JavaFX and ScalaFX. 33 | // ScalaFX `remove` corresponds to `scala.collection.mutable.Buffer.remove(n:Int, count:Int)`. 34 | // Equivalent of JavaFX `remove` is renamed in ScalaFX to `removeRange`. 35 | println("Calling removeRange(2, 4): ") 36 | strings.removeRange(2, 4) 37 | 38 | println("""Remove elements that contain letter "t"""") 39 | strings --= strings.filter(_.contains("t")) 40 | 41 | println("""Calling --= Seq("Third", "Fourth"): """) 42 | strings --= Seq("Third", "Fourth") 43 | } 44 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/ResponsiveUIExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.application.{JFXApp3, Platform} 6 | import scalafx.beans.property.ObjectProperty 7 | import scalafx.geometry.{Insets, Pos} 8 | import scalafx.scene.Scene 9 | import scalafx.scene.control.Button 10 | import scalafx.scene.layout.{BorderPane, HBox} 11 | import scalafx.scene.paint.Color 12 | import scalafx.scene.shape.Rectangle 13 | 14 | /** 15 | * @author Jarek Sacha 16 | */ 17 | object ResponsiveUIExample extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | 21 | hookupEvents() 22 | 23 | stage = new PrimaryStage { 24 | title = "Unresponsive UI Example" 25 | scene = View.scene 26 | } 27 | } 28 | 29 | def hookupEvents(): Unit = { 30 | View.changeFillButton.onAction = () => { 31 | val fillPaint = Model.fillPaint() 32 | Model.fillPaint() = if (Color.LightGray.delegate == fillPaint) Color.Gray else Color.LightGray 33 | 34 | val task = new Runnable { 35 | def run(): Unit = { 36 | try { 37 | Thread.sleep(3000) 38 | Platform.runLater { 39 | val rect = View.rectangle 40 | val newArcSize = if (rect.arcHeight() < 20) 30 else 0 41 | rect.arcWidth() = newArcSize 42 | rect.arcHeight() = newArcSize 43 | } 44 | } catch { 45 | case e: InterruptedException => /* Properly handle exception */ 46 | } 47 | } 48 | } 49 | new Thread(task).start() 50 | } 51 | 52 | View.changeStrokeButton.onAction = () => { 53 | val strokePaint = Model.strokePaint() 54 | Model.strokePaint() = if (strokePaint == Color.DarkGray.delegate) Color.Black else Color.DarkGray 55 | } 56 | } 57 | 58 | private object Model { 59 | // `fill` and `stroke` are created using ObjectProperty factory method to ensure proper type parameter 60 | // to ObjectProperty. We use here, implicitly, JavaFX Paint as the type for `ObjectProperty`. 61 | val fillPaint = ObjectProperty(this, "fillPaint", Color.LightGray) 62 | val strokePaint = ObjectProperty(this, "strokePaint", Color.DarkGray) 63 | } 64 | 65 | private object View { 66 | val rectangle = new Rectangle { 67 | width = 200 68 | height = 200 69 | strokeWidth = 10 70 | fill <== Model.fillPaint 71 | stroke <== Model.strokePaint 72 | } 73 | 74 | val changeFillButton = new Button("Change Fill") 75 | val changeStrokeButton = new Button("Chang Stroke") 76 | 77 | val buttonHBox = new HBox { 78 | padding = Insets(10) 79 | spacing = 10 80 | alignment = Pos.Center 81 | children = List( 82 | changeFillButton, 83 | changeStrokeButton 84 | ) 85 | } 86 | 87 | val scene = new Scene { 88 | root = new BorderPane { 89 | padding = Insets(10) 90 | center = rectangle 91 | bottom = buttonHBox 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/SetChangeEventExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.collections.ObservableSet 4 | import scalafx.collections.ObservableSet.* 5 | 6 | object SetChangeEventExample extends App { 7 | 8 | def prettyChange(change: Change[_]): String = { 9 | val sb = new StringBuffer("\tChange event data:\n") 10 | 11 | change match { 12 | case Add(added) => 13 | sb.append("\t\tWas added\n") 14 | sb.append("\t\tValue added : %s\n".format(added)) 15 | case Remove(removed) => 16 | sb.append("\t\tWas removed\n") 17 | sb.append("\t\tValue removed: %s\n".format(removed)) 18 | } 19 | 20 | sb.toString 21 | } 22 | 23 | def onChange[T](set: ObservableSet[T], change: Change[T]): Unit = { 24 | println("\tset = " + set) 25 | println(prettyChange(change)) 26 | } 27 | 28 | val set = ObservableSet.empty[String] 29 | set.onChange(onChange(_, _)) 30 | 31 | println("Calling set += \"First\": ") 32 | set += "First" 33 | 34 | println("Calling set ++= Seq(\"Second\", \"Third\"): ") 35 | set ++= Seq("Second", "Third") 36 | 37 | println("Calling set -= \"Second\": ") 38 | set -= "Second" 39 | 40 | println("Calling set --= Seq(\"First\", \"Third\"): ") 41 | set --= Seq("First", "Third") 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/proscalafx/ch06/UnresponsiveUIExample.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch06 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.beans.property.ObjectProperty 7 | import scalafx.geometry.{Insets, Pos} 8 | import scalafx.scene.Scene 9 | import scalafx.scene.control.Button 10 | import scalafx.scene.layout.{BorderPane, HBox} 11 | import scalafx.scene.paint.Color 12 | import scalafx.scene.shape.Rectangle 13 | 14 | /** 15 | * @author Jarek Sacha 16 | */ 17 | object UnresponsiveUIExample extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | 21 | hookupEvents() 22 | 23 | stage = new PrimaryStage { 24 | title = "Unresponsive UI Example" 25 | scene = View.scene 26 | } 27 | } 28 | 29 | def hookupEvents(): Unit = { 30 | View.changeFillButton.onAction = () => { 31 | val fillPaint = Model.fillPaint() 32 | Model.fillPaint() = if (fillPaint == Color.LightGray.delegate) Color.Gray else Color.LightGray 33 | // Bad code that will cause the UI to be unresponsive 34 | try { 35 | Thread.sleep(Long.MaxValue) 36 | } catch { 37 | case _: InterruptedException => /* Properly handle exception */ 38 | } 39 | } 40 | 41 | View.changeStrokeButton.onAction = () => { 42 | val strokePaint = Model.strokePaint() 43 | Model.strokePaint() = if (strokePaint == Color.DarkGray.delegate) Color.Black else Color.DarkGray 44 | } 45 | } 46 | 47 | private object Model { 48 | // `fill` and `stroke` are created using ObjectProperty factory method to ensure proper type parameter 49 | // to ObjectProperty. We use here, implicitly, JavaFX Paint as the type for `ObjectProperty`. 50 | val fillPaint = ObjectProperty(this, "fillPaint", Color.LightGray) 51 | val strokePaint = ObjectProperty(this, "strokePaint", Color.DarkGray) 52 | } 53 | 54 | private object View { 55 | val rectangle = new Rectangle { 56 | width = 200 57 | height = 200 58 | strokeWidth = 10 59 | fill <== Model.fillPaint 60 | stroke <== Model.strokePaint 61 | } 62 | 63 | val changeFillButton = new Button("Change Fill") 64 | val changeStrokeButton = new Button("Chang Stroke") 65 | 66 | val buttonHBox = new HBox { 67 | padding = Insets(10) 68 | spacing = 10 69 | alignment = Pos.Center 70 | children = List( 71 | changeFillButton, 72 | changeStrokeButton 73 | ) 74 | } 75 | 76 | val scene = new Scene { 77 | root = new BorderPane { 78 | padding = Insets(10) 79 | center = rectangle 80 | bottom = buttonHBox 81 | } 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp1.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.collections.ObservableBuffer 6 | import scalafx.scene.Scene 7 | import scalafx.scene.chart.PieChart 8 | import scalafx.scene.layout.StackPane 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | object ChartApp1 extends JFXApp3 { 14 | 15 | override def start(): Unit = { 16 | 17 | stage = new PrimaryStage { 18 | title = "PieChart" 19 | scene = new Scene(400, 250) { 20 | root = new StackPane { 21 | children = new PieChart() { 22 | data = chartData() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | private def chartData() = ObservableBuffer( 30 | PieChart.Data("java", 17.56), 31 | PieChart.Data("C", 17.06), 32 | PieChart.Data("C++", 8.25), 33 | PieChart.Data("C#", 8.20), 34 | PieChart.Data("ObjectiveC", 6.8), 35 | PieChart.Data("PHP", 6.0), 36 | PieChart.Data("(Visual)Basic", 4.76), 37 | PieChart.Data("Other", 31.37) 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp10.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | import scalafx.util.StringConverter 12 | 13 | /** 14 | * We use here the version of the example that is described in the book, it scales axis so bubbles appear as spheres 15 | * rather than ellipses (in the downloadable Java code). 16 | * 17 | * @author Jarek Sacha 18 | */ 19 | object ChartApp10 extends JFXApp3 { 20 | 21 | private val xStep = 10 22 | private val xMin = 2010 * xStep 23 | private val xMax = 2016 * xStep 24 | 25 | override def start(): Unit = { 26 | 27 | val xAxis = new NumberAxis { 28 | autoRanging = false 29 | lowerBound = xMin - xStep 30 | upperBound = xMax + xStep 31 | tickUnit = xStep 32 | // The xAxis ranges from 20110 till 20210, but of course we want to show the years at the axis. This can 33 | // be achieved by calling 34 | tickLabelFormatter = new StringConverter[Number] { 35 | // Here we do not need to convert from string. 36 | def fromString(string: String): Number = throw new UnsupportedOperationException("Not implemented.") 37 | 38 | def toString(t: Number): String = (t.intValue() / xStep).toString 39 | } 40 | } 41 | val yAxis = new NumberAxis() 42 | val bubbleChart = BubbleChart(xAxis, yAxis) 43 | bubbleChart.title = "Speculations" 44 | bubbleChart.data = createChartData() 45 | 46 | stage = new PrimaryStage { 47 | title = "BubbleChart example" 48 | scene = new Scene(400, 250) { 49 | root = new StackPane { 50 | children = bubbleChart 51 | } 52 | } 53 | } 54 | } 55 | 56 | // NOTE: explicit type signature using Number instead Int and Double 57 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 58 | // signature for scalafx.scene.chart.XYChart.data used above. 59 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[Number, Number]] = { 60 | var javaValue = 17.56 61 | var cValue = 17.06 62 | var cppValue = 8.25 63 | val scale = 10 64 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]() 65 | val java = new XYChart.Series[Number, Number] { 66 | name = "Java" 67 | } 68 | val c = new XYChart.Series[Number, Number] { 69 | name = "C" 70 | } 71 | val cpp = new XYChart.Series[Number, Number] { 72 | name = "C++" 73 | } 74 | for (i <- xMin to xMax by xStep) { 75 | java.data() += XYChart.Data[Number, Number](i, javaValue) 76 | javaValue += +scale * math.random() - scale / 2 77 | 78 | c.data() += XYChart.Data[Number, Number](i, cValue) 79 | cValue += scale * math.random() - scale / 2 80 | 81 | cpp.data() += XYChart.Data[Number, Number](i, cppValue) 82 | cppValue += scale * math.random() - scale / 2 83 | } 84 | answer.addAll(java, c, cpp) 85 | answer 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp11.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | import scalafx.util.StringConverter 12 | 13 | /** 14 | * We use here the version of the example that is described in the book, it scales axis so bubbles appear as spheres 15 | * rather than ellipses (in the downloadable Java code). 16 | * 17 | * @author Jarek Sacha 18 | */ 19 | object ChartApp11 extends JFXApp3 { 20 | 21 | val xStep = 10 22 | val xMin = 2010 * xStep 23 | val xMax = 2016 * xStep 24 | 25 | override def start(): Unit = { 26 | 27 | val xAxis = new NumberAxis() { 28 | autoRanging = false 29 | lowerBound = xMin - xStep 30 | upperBound = xMax + xStep 31 | tickUnit = xStep 32 | // The xAxis ranges from 20110 till 20210, but of course we want to show the years at the axis. This can 33 | // be achieved by calling 34 | tickLabelFormatter = new StringConverter[Number] { 35 | // Here we do not need to convert from string. 36 | def fromString(string: String): Number = throw new UnsupportedOperationException("Not implemented.") 37 | 38 | def toString(t: Number): String = (t.intValue() / xStep).toString 39 | } 40 | } 41 | val yAxis = new NumberAxis() 42 | val bubbleChart = BubbleChart(xAxis, yAxis) 43 | bubbleChart.title = "Speculations" 44 | bubbleChart.data = createChartData() 45 | 46 | stage = new PrimaryStage { 47 | title = "BubbleChart example" 48 | scene = new Scene(400, 250) { 49 | root = new StackPane { 50 | children = bubbleChart 51 | } 52 | } 53 | } 54 | } 55 | 56 | // NOTE: explicit type signature using Number instead Int and Double 57 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 58 | // signature for scalafx.scene.chart.XYChart.data used above. 59 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[Number, Number]] = { 60 | var javaValue = 17.56 61 | var cValue = 17.06 62 | var cppValue = 8.25 63 | val scale = 10 64 | 65 | def dif: Double = scale * math.random() / 4 66 | 67 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]() 68 | val java = new XYChart.Series[Number, Number] { 69 | name = "Java" 70 | } 71 | val c = new XYChart.Series[Number, Number] { 72 | name = "C" 73 | } 74 | val cpp = new XYChart.Series[Number, Number] { 75 | name = "C++" 76 | } 77 | for (i <- xMin to xMax by xStep) { 78 | // NOTE: XYChart.Data declares its third parameter as a `AnyRef`. A Double returned by `math.rand` 79 | // is not a subclass of `AnyRef`, so we need to explicitly convert it to a `java.lang.Number` to make it work. 80 | java.data() += XYChart.Data[Number, Number](i, javaValue, dif.asInstanceOf[Number]) 81 | javaValue += +scale * math.random() - scale / 2 82 | 83 | c.data() += XYChart.Data[Number, Number](i, cValue, dif.asInstanceOf[Number]) 84 | cValue += scale * math.random() - scale / 2 85 | 86 | cpp.data() += XYChart.Data[Number, Number](i, cppValue, dif.asInstanceOf[Number]) 87 | cppValue += scale * math.random() - scale / 2 88 | } 89 | answer.addAll(java, c, cpp) 90 | answer 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp2.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.collections.ObservableBuffer 6 | import scalafx.geometry.Side 7 | import scalafx.scene.Scene 8 | import scalafx.scene.chart.PieChart 9 | import scalafx.scene.layout.StackPane 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | object ChartApp2 extends JFXApp3 { 15 | 16 | override def start(): Unit = { 17 | 18 | stage = new PrimaryStage { 19 | title = "Chart App 2" 20 | scene = new Scene(400, 350) { 21 | root = new StackPane { 22 | children = new PieChart() { 23 | data = chartData() 24 | title = "Tiobe index" 25 | legendSide = Side.Left 26 | clockwise = false 27 | labelsVisible = false 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | private def chartData() = ObservableBuffer( 35 | PieChart.Data("java", 17.56), 36 | PieChart.Data("C", 17.06), 37 | PieChart.Data("C++", 8.25), 38 | PieChart.Data("C#", 8.20), 39 | PieChart.Data("ObjectiveC", 6.8), 40 | PieChart.Data("PHP", 6.0), 41 | PieChart.Data("(Visual)Basic", 4.76), 42 | PieChart.Data("Other", 31.37) 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp3.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.{NumberAxis, ScatterChart, XYChart} 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp3 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new NumberAxis() 20 | val yAxis = new NumberAxis() 21 | val scatterChart = ScatterChart(xAxis, yAxis) 22 | 23 | scatterChart.data = createChartData() 24 | 25 | stage = new PrimaryStage { 26 | title = "Chart App 3" 27 | scene = new Scene(400, 250) { 28 | root = new StackPane { 29 | children = scatterChart 30 | } 31 | } 32 | } 33 | } 34 | 35 | // NOTE: explicit type signature using Number instead Int and Double 36 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 37 | // signature for scalafx.scene.chart.XYChart.data used above. 38 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[Number, Number]] = { 39 | var javaValue = 17.56 40 | var cValue = 17.06 41 | var cppValue = 8.25 42 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]() 43 | val java = new XYChart.Series[Number, Number] { 44 | name = "Java" 45 | } 46 | val c = new XYChart.Series[Number, Number] { 47 | name = "C" 48 | } 49 | val cpp = new XYChart.Series[Number, Number] { 50 | name = "C++" 51 | } 52 | for (i <- 2011 to 2020) { 53 | java.data() += XYChart.Data[Number, Number](i, javaValue) 54 | javaValue += math.random() - .5 55 | 56 | c.data() += XYChart.Data[Number, Number](i, cValue) 57 | cValue += math.random() - .5 58 | 59 | cpp.data() += XYChart.Data[Number, Number](i, cppValue) 60 | cppValue += math.random() - .5 61 | } 62 | answer.addAll(java, c, cpp) 63 | answer 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp4.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.collections.ObservableBuffer 6 | import scalafx.scene.Scene 7 | import scalafx.scene.chart.{NumberAxis, ScatterChart, XYChart} 8 | import scalafx.scene.layout.StackPane 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | object ChartApp4 extends JFXApp3 { 14 | 15 | override def start(): Unit = { 16 | 17 | val xAxis = new NumberAxis { 18 | autoRanging = false 19 | lowerBound = 2011 20 | upperBound = 2021 21 | } 22 | val yAxis = new NumberAxis() 23 | val scatterChart = ScatterChart[Number, Number](xAxis, yAxis, createChartData()) 24 | 25 | stage = new PrimaryStage { 26 | title = "Chart App 4" 27 | scene = new Scene(400, 250) { 28 | root = new StackPane { 29 | children = scatterChart 30 | } 31 | } 32 | } 33 | } 34 | 35 | private def createChartData() = { 36 | 37 | val years = 2011 to 2020 38 | 39 | // Generate trend by creating a cumulative sum of random values 40 | def generateTrend(startValue: Double) = years.map(_ => math.random() - .5).scanLeft(startValue)(_ + _) 41 | 42 | val javaTrend = generateTrend(17.56) 43 | val cTrend = generateTrend(17.06) 44 | val cppTrend = generateTrend(8.25) 45 | 46 | // NOTE: explicit type signature using Number instead Int and Double 47 | // We are deliberately using here factory methods, instead of "new", to create instances of 48 | // javafx.scene.chart.* types. 49 | val javaData = years zip javaTrend map { case (y, d) => XYChart.Data[Number, Number](y, d) } 50 | val cData = years zip cTrend map { case (y, d) => XYChart.Data[Number, Number](y, d) } 51 | val cppData = years zip cppTrend map { case (y, d) => XYChart.Data[Number, Number](y, d) } 52 | 53 | ObservableBuffer( 54 | XYChart.Series[Number, Number](name = "Java", data = ObservableBuffer.from(javaData)), 55 | XYChart.Series[Number, Number](name = "C", data = ObservableBuffer.from(cData)), 56 | XYChart.Series[Number, Number](name = "C++", data = ObservableBuffer.from(cppData)) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp5.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.{NumberAxis, ScatterChart, XYChart} 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp5 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new NumberAxis { 20 | autoRanging = false 21 | lowerBound = 2011 22 | upperBound = 2021 23 | } 24 | val yAxis = new NumberAxis() 25 | 26 | val scatterChart = ScatterChart(xAxis, yAxis) 27 | scatterChart.title = "Speculations" 28 | scatterChart.data = createChartData() 29 | 30 | stage = new PrimaryStage { 31 | title = "ScatterChart example" 32 | scene = new Scene(400, 250) { 33 | root = new StackPane { 34 | children = scatterChart 35 | } 36 | } 37 | } 38 | } 39 | 40 | // NOTE: explicit type signature using Number instead Int and Double 41 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 42 | // signature for scalafx.scene.chart.XYChart.data used above. 43 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[Number, Number]] = { 44 | var javaValue: Double = 17.56 45 | var cValue: Double = 17.06 46 | var cppValue: Double = 8.25 47 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[Number, Number]]() 48 | val java = new XYChart.Series[Number, Number] { 49 | name = "Java" 50 | } 51 | val c = new XYChart.Series[Number, Number] { 52 | name = "C" 53 | } 54 | val cpp = new XYChart.Series[Number, Number] { 55 | name = "C++" 56 | } 57 | for (i <- 2011 to 2021) { 58 | java.data() += XYChart.Data[Number, Number](i, javaValue) 59 | javaValue += math.random() - .5 60 | 61 | c.data() += XYChart.Data[Number, Number](i, cValue) 62 | cValue += math.random() - .5 63 | 64 | cpp.data() += XYChart.Data[Number, Number](i, cppValue) 65 | cppValue += math.random() - .5 66 | } 67 | answer.addAll(java, c, cpp) 68 | answer 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp6.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp6 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new CategoryAxis() 20 | val yAxis = new NumberAxis() 21 | val scatterChart = ScatterChart(xAxis, yAxis) 22 | scatterChart.title = "Speculations" 23 | scatterChart.data = createChartData() 24 | 25 | stage = new PrimaryStage { 26 | title = "ScatterChart example" 27 | scene = new Scene(400, 250) { 28 | root = new StackPane { 29 | children = scatterChart 30 | } 31 | } 32 | } 33 | } 34 | 35 | // NOTE: explicit type signature using Number instead Int and Double 36 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 37 | // signature for scalafx.scene.chart.XYChart.data used above. 38 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[String, Number]] = { 39 | var javaValue = 17.56 40 | var cValue = 17.06 41 | var cppValue = 8.25 42 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[String, Number]]() 43 | val java = new XYChart.Series[String, Number] { 44 | name = "Java" 45 | } 46 | val c = new XYChart.Series[String, Number] { 47 | name = "C" 48 | } 49 | val cpp = new XYChart.Series[String, Number] { 50 | name = "C++" 51 | } 52 | for (i <- 2011 to 2021) { 53 | java.data() += XYChart.Data[String, Number](i.toString, javaValue) 54 | javaValue += math.random() - .5 55 | 56 | c.data() += XYChart.Data[String, Number](i.toString, cValue) 57 | cValue += math.random() - .5 58 | 59 | cpp.data() += XYChart.Data[String, Number](i.toString, cppValue) 60 | cppValue += math.random() - .5 61 | } 62 | answer.addAll(java, c, cpp) 63 | answer 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp7.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp7 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new CategoryAxis() 20 | val yAxis = new NumberAxis() 21 | val lineChart = LineChart(xAxis, yAxis) 22 | lineChart.title = "Speculations" 23 | lineChart.data = createChartData() 24 | 25 | stage = new PrimaryStage { 26 | title = "LineChart example" 27 | scene = new Scene(400, 250) { 28 | root = new StackPane { 29 | children = lineChart 30 | } 31 | } 32 | } 33 | } 34 | 35 | // NOTE: explicit type signature using Number instead Int and Double 36 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 37 | // signature for scalafx.scene.chart.XYChart.data used above. 38 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[String, Number]] = { 39 | var javaValue = 17.56 40 | var cValue = 17.06 41 | var cppValue = 8.25 42 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[String, Number]]() 43 | val java = new XYChart.Series[String, Number] { 44 | name = "Java" 45 | } 46 | val c = new XYChart.Series[String, Number] { 47 | name = "C" 48 | } 49 | val cpp = new XYChart.Series[String, Number] { 50 | name = "C++" 51 | } 52 | for (i <- 2011 to 2021) { 53 | java.data() += XYChart.Data[String, Number](i.toString, javaValue) 54 | javaValue += math.random() - .5 55 | 56 | c.data() += XYChart.Data[String, Number](i.toString, cValue) 57 | cValue += math.random() - .5 58 | 59 | cpp.data() += XYChart.Data[String, Number](i.toString, cppValue) 60 | cppValue += math.random() - .5 61 | } 62 | answer.addAll(java, c, cpp) 63 | answer 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp8.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp8 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new CategoryAxis() 20 | val yAxis = new NumberAxis() 21 | val barChart = BarChart(xAxis, yAxis) 22 | barChart.title = "Speculations" 23 | barChart.data = createChartData() 24 | barChart.barGap = 1 25 | 26 | stage = new PrimaryStage { 27 | title = "BarChart example" 28 | scene = new Scene(400, 250) { 29 | root = new StackPane { 30 | children = barChart 31 | } 32 | } 33 | } 34 | } 35 | 36 | // NOTE: explicit type signature using Number instead Int and Double 37 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 38 | // signature for scalafx.scene.chart.XYChart.data used above. 39 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[String, Number]] = { 40 | var javaValue = 17.56 41 | var cValue = 17.06 42 | var cppValue = 8.25 43 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[String, Number]]() 44 | val java = new XYChart.Series[String, Number] { 45 | name = "Java" 46 | } 47 | val c = new XYChart.Series[String, Number] { 48 | name = "C" 49 | } 50 | val cpp = new XYChart.Series[String, Number] { 51 | name = "C++" 52 | } 53 | for (i <- 2011 to 2016) { 54 | java.data() += XYChart.Data[String, Number](i.toString, javaValue) 55 | javaValue += math.random() - .5 56 | 57 | c.data() += XYChart.Data[String, Number](i.toString, cValue) 58 | cValue += math.random() - .5 59 | 60 | cpp.data() += XYChart.Data[String, Number](i.toString, cppValue) 61 | cppValue += math.random() - .5 62 | } 63 | answer.addAll(java, c, cpp) 64 | answer 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/proscalafx/ch07/ChartApp9.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch07 2 | 3 | import javafx.scene.chart as jfxsc 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.collections.ObservableBuffer 8 | import scalafx.scene.Scene 9 | import scalafx.scene.chart.* 10 | import scalafx.scene.layout.StackPane 11 | 12 | /** 13 | * @author Jarek Sacha 14 | */ 15 | object ChartApp9 extends JFXApp3 { 16 | 17 | override def start(): Unit = { 18 | 19 | val xAxis = new CategoryAxis() 20 | val yAxis = new NumberAxis() 21 | val areaChart = AreaChart(xAxis, yAxis) 22 | areaChart.title = "Speculations" 23 | areaChart.data = createChartData() 24 | 25 | stage = new PrimaryStage { 26 | title = "AreaChart example" 27 | scene = new Scene(400, 250) { 28 | root = new StackPane { 29 | children = areaChart 30 | } 31 | } 32 | } 33 | } 34 | 35 | // NOTE: explicit type signature using Number instead Int and Double 36 | // NOTE: use of jfxsc.XYChart.Series as type for ObservableBuffer, this the same as 37 | // signature for scalafx.scene.chart.XYChart.data used above. 38 | private def createChartData(): ObservableBuffer[jfxsc.XYChart.Series[String, Number]] = { 39 | var javaValue = 17.56 40 | var cValue = 17.06 41 | var cppValue = 8.25 42 | val answer = new ObservableBuffer[jfxsc.XYChart.Series[String, Number]]() 43 | val java = new XYChart.Series[String, Number] { 44 | name = "Java" 45 | } 46 | val c = new XYChart.Series[String, Number] { 47 | name = "C" 48 | } 49 | val cpp = new XYChart.Series[String, Number] { 50 | name = "C++" 51 | } 52 | for (i <- 2011 to 2020) { 53 | java.data() += XYChart.Data[String, Number](i.toString, javaValue) 54 | javaValue += math.random() - .5 55 | 56 | c.data() += XYChart.Data[String, Number](i.toString, cValue) 57 | cValue += math.random() - .5 58 | 59 | cpp.data() += XYChart.Data[String, Number](i.toString, cppValue) 60 | cppValue += math.random() - .5 61 | } 62 | answer.addAll(java, c, cpp) 63 | answer 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer1/AudioPlayer1.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer1 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.scene.Scene 6 | import scalafx.scene.media.{Media, MediaPlayer} 7 | 8 | /** 9 | * A very simple application that plays an audio file. 10 | * 11 | * @author Jarek Sacha 12 | */ 13 | object AudioPlayer1 extends JFXApp3 { 14 | 15 | override def start(): Unit = { 16 | 17 | val resource = getClass.getResource("resources/keeper.mp3") 18 | val media = new Media(resource.toString) 19 | val mediaPlayer = new MediaPlayer(media) 20 | mediaPlayer.play() 21 | 22 | stage = new PrimaryStage { 23 | title = "Audio Player 1" 24 | scene = new Scene(200, 200) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer1/resources/keeper.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer1/resources/keeper.mp3 -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer2/AudioPlayer2.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer2 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.collections.ObservableMap.Add 7 | import scalafx.geometry.{Insets, VPos} 8 | import scalafx.scene.Scene 9 | import scalafx.scene.control.Label 10 | import scalafx.scene.effect.Reflection 11 | import scalafx.scene.image.{Image, ImageView} 12 | import scalafx.scene.layout.{ColumnConstraints, GridPane, Priority, RowConstraints} 13 | import scalafx.scene.media.{Media, MediaPlayer} 14 | 15 | /** 16 | * Displaying the metadata information in the scene graph. 17 | * 18 | * @author Jarek Sacha 19 | */ 20 | object AudioPlayer2 extends JFXApp3 { 21 | 22 | private var media: Media = _ 23 | private var mediaPlayer: MediaPlayer = _ 24 | private var artist: Label = _ 25 | private var album: Label = _ 26 | private var title: Label = _ 27 | private var year: Label = _ 28 | private var albumCover: ImageView = _ 29 | 30 | override def start(): Unit = { 31 | 32 | createControls() 33 | createMedia() 34 | 35 | stage = new PrimaryStage { 36 | self => 37 | self.title = "Audio Player 2" 38 | scene = new Scene(createGridPane(), 800, 400) { 39 | val stylesheet = getClass.getResource("media.css") 40 | stylesheets += stylesheet.toString 41 | } 42 | } 43 | } 44 | 45 | private def createGridPane(): GridPane = new GridPane { 46 | padding = Insets(10) 47 | hgap = 20 48 | add(albumCover, 0, 0, 1, GridPane.Remaining) 49 | add(title, 1, 0) 50 | add(artist, 1, 1) 51 | add(album, 1, 2) 52 | add(year, 1, 3) 53 | columnConstraints ++= Seq( 54 | new ColumnConstraints(), 55 | // NOTE: the call to delegate to avoid compilation error. 56 | // Should `scalafx.scene.layout.GridPane.columnConstraints_=()` be fixed to work without call to delegate? 57 | new ColumnConstraints { 58 | hgrow = Priority.Always 59 | }.delegate 60 | ) 61 | val r0 = new RowConstraints { 62 | valignment = VPos.Top 63 | } 64 | rowConstraints ++= Seq(r0, r0, r0, r0) 65 | } 66 | 67 | private def createControls(): Unit = { 68 | artist = new Label { 69 | id = "artist" 70 | } 71 | album = new Label { 72 | id = "album" 73 | } 74 | title = new Label { 75 | id = "title" 76 | } 77 | year = new Label { 78 | id = "year" 79 | } 80 | val url = getClass.getResource("resources/defaultAlbum.png") 81 | albumCover = new ImageView { 82 | image = new Image(url.toString) 83 | fitWidth = 240 84 | preserveRatio = true 85 | smooth = true 86 | effect = new Reflection { 87 | fraction = 0.2 88 | } 89 | } 90 | } 91 | 92 | private def createMedia(): Unit = { 93 | try { 94 | media = new Media("https://traffic.libsyn.com/dickwall/JavaPosse373.mp3") { 95 | metadata.onChange((_, change) => { 96 | change match { 97 | case Add(key, added) => handleMetadata(key, added) 98 | case _ => 99 | } 100 | }) 101 | } 102 | 103 | mediaPlayer = new MediaPlayer(media) { 104 | self => 105 | onError = { 106 | val errorMessage: String = self.media.getError.getMessage 107 | System.out.println("MediaPlayer Error: " + errorMessage) 108 | } 109 | } 110 | 111 | mediaPlayer.play() 112 | } catch { 113 | // Handle construction errors 114 | case re: RuntimeException => println("Caught Exception: " + re.getMessage) 115 | } 116 | } 117 | 118 | private def handleMetadata(key: String, value: AnyRef): Unit = { 119 | key match { 120 | case "album" => album.text = value.toString 121 | case "artist" => artist.text = value.toString 122 | case "title" => title.text = value.toString 123 | case "year" => year.text = value.toString 124 | case "image" => albumCover.image = value.asInstanceOf[javafx.scene.image.Image] 125 | case _ => println("Unhandled metadata key: " + key + ", value: " + value) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer2/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #aaa; 3 | -fx-background-color: black; 4 | -fx-background-color: radial-gradient(center 50% 50%, radius 60%, #111, #000); 5 | -fx-background-image: url("resources/cross.png"); 6 | } 7 | 8 | .button { 9 | -fx-text-fill: #E0E0E0; 10 | -fx-font-size: 20pt; 11 | -fx-pref-width: 300px; 12 | } 13 | 14 | .label { 15 | -fx-font-size: 18pt; 16 | -fx-text-fill: #aaa; 17 | } 18 | 19 | #title { 20 | -fx-font-size: 24pt; 21 | -fx-text-fill: #ddd; 22 | } 23 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer2/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer2/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer2/resources/defaultAlbum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer2/resources/defaultAlbum.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/AbstractView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer3 2 | 3 | import scalafx.scene.Node 4 | 5 | /** 6 | * @author Jarek Sacha 7 | */ 8 | abstract class AbstractView(protected val songMadel: SongModel) { 9 | private val _viewNode: Node = initView() 10 | 11 | def viewNode: Node = _viewNode 12 | 13 | protected def initView(): Node 14 | } 15 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/AudioPlayer3.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer3 2 | 3 | import com.sun.javafx.runtime.VersionInfo 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.scene.Scene 8 | import scalafx.scene.input.TransferMode 9 | import scalafx.scene.layout.BorderPane 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | object AudioPlayer3 extends JFXApp3 { 15 | 16 | override def start(): Unit = { 17 | 18 | val songModel = new SongModel() { 19 | url = "https://traffic.libsyn.com/dickwall/JavaPosse373.mp3" 20 | } 21 | 22 | println("JavaFX version: " + VersionInfo.getRuntimeVersion) 23 | 24 | val metaDataView = new MetadataView(songModel) 25 | val playerControlsView = new PlayerControlsView(songModel) 26 | 27 | val root = new BorderPane { 28 | center = metaDataView.viewNode 29 | bottom = playerControlsView.viewNode 30 | } 31 | 32 | stage = new PrimaryStage { 33 | title = "Audio Player 3" 34 | scene = new Scene(root, 800, 400) { 35 | val stylesheet = getClass.getResource("media.css") 36 | stylesheets += stylesheet.toString 37 | } 38 | initSceneDragAndDrop(scene(), songModel) 39 | } 40 | } 41 | 42 | private def initSceneDragAndDrop(scene: Scene, songModel: SongModel): Unit = { 43 | scene.onDragOver = event => { 44 | val db = event.dragboard 45 | if (db.hasFiles || db.hasUrl) { 46 | // NOTE: We need to pass in `TransferMode` as Java vararg, since we use `javafx.scene.input.DragEvent` 47 | event.acceptTransferModes(TransferMode.Any: _*) 48 | } 49 | event.consume() 50 | } 51 | 52 | scene.onDragDropped = event => { 53 | val db = event.dragboard 54 | val url = 55 | if (db.hasFiles) { 56 | db.getFiles.get(0).toURI.toString 57 | } else if (db.hasUrl) { 58 | db.getUrl 59 | } else { 60 | null 61 | } 62 | 63 | if (url != null) { 64 | songModel.url = url 65 | songModel.mediaPlayer().play() 66 | } 67 | event.dropCompleted = url != null 68 | event.consume() 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/MetadataView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer3 2 | 3 | import scalafx.Includes.* 4 | import scalafx.geometry.{Insets, VPos} 5 | import scalafx.scene.Node 6 | import scalafx.scene.control.Label 7 | import scalafx.scene.effect.Reflection 8 | import scalafx.scene.image.ImageView 9 | import scalafx.scene.layout.{ColumnConstraints, GridPane, Priority, RowConstraints} 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | class MetadataView(songModel: SongModel) extends AbstractView(songModel) { 15 | 16 | def initView(): Node = { 17 | val title = new Label { 18 | text <== songModel.title 19 | id = "title" 20 | } 21 | val artist = new Label { 22 | text <== songModel.artist 23 | id = "artist" 24 | } 25 | val album = new Label { 26 | text <== songModel.album 27 | id = "album" 28 | } 29 | val year = new Label { 30 | text <== songModel.year 31 | id = "year" 32 | } 33 | val albumCover = new ImageView { 34 | image <== songModel.albumCover 35 | fitWidth = 240 36 | preserveRatio = true 37 | smooth = true 38 | effect = new Reflection { 39 | fraction = 0.2 40 | } 41 | } 42 | 43 | new GridPane { 44 | padding = Insets(10) 45 | hgap = 20 46 | add(albumCover, 0, 0, 1, GridPane.Remaining) 47 | add(title, 1, 0) 48 | add(artist, 1, 1) 49 | add(album, 1, 2) 50 | add(year, 1, 3) 51 | columnConstraints ++= Seq( 52 | new ColumnConstraints(), 53 | // NOTE: the call to delegate to avoid compilation error. 54 | // Should `scalafx.scene.layout.GridPane.columnConstraints_=()` be fixed to work without call to delegate? 55 | new ColumnConstraints { 56 | hgrow = Priority.Always 57 | }.delegate 58 | ) 59 | val r0 = new RowConstraints { 60 | valignment = VPos.Top 61 | } 62 | rowConstraints ++= Seq(r0, r0, r0, r0) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/SongModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer3 2 | 3 | import javafx.scene.image as jfxsi 4 | import scalafx.beans.property.{ObjectProperty, ReadOnlyObjectWrapper, StringProperty} 5 | import scalafx.collections.ObservableMap.Add 6 | import scalafx.scene.image.Image 7 | import scalafx.scene.media.{Media, MediaPlayer} 8 | 9 | /** 10 | * @author Jarek Sacha 11 | */ 12 | class SongModel { 13 | private val DefaultImageURL = classOf[SongModel].getResource("resources/defaultAlbum.png").toString 14 | private val DefaultImageCover = new Image(DefaultImageURL) 15 | val album = new StringProperty(this, "album") 16 | val artist = new StringProperty(this, "artist") 17 | val title = new StringProperty(this, "title") 18 | val year = new StringProperty(this, "year") 19 | // NOTE: use of `javafx.scene.image.Image` instead of `scalafx.scene.image.Image`, this is required for binding in 20 | // MetadataView to compile. 21 | val albumCover = new ObjectProperty[jfxsi.Image](this, "albumCover") 22 | private val _mediaPlayer = new ReadOnlyObjectWrapper[MediaPlayer](this, "mediaPlayer") 23 | 24 | resetProperties() 25 | 26 | def mediaPlayer = _mediaPlayer.readOnlyProperty 27 | 28 | def url: String = if (mediaPlayer() != null) mediaPlayer().media.source else null 29 | 30 | def url_=(url: String): Unit = { 31 | if (mediaPlayer() != null) mediaPlayer().stop() 32 | 33 | initializeMedia(url) 34 | } 35 | 36 | private def resetProperties(): Unit = { 37 | artist() = "" 38 | album() = "" 39 | title() = "" 40 | year() = "" 41 | albumCover() = DefaultImageCover 42 | } 43 | 44 | private def initializeMedia(url: String): Unit = { 45 | resetProperties() 46 | 47 | try { 48 | val media = new Media(url) { 49 | metadata.onChange((_, change) => { 50 | change match { 51 | case Add(key, added) => handleMetadata(key, added) 52 | case _ => 53 | } 54 | }) 55 | } 56 | 57 | _mediaPlayer() = new MediaPlayer(media) { 58 | self => 59 | // Handle errors during playback 60 | onError = { 61 | val errorMessage = self.media.error().getMessage 62 | println("MediaPlayer Error: " + errorMessage) 63 | } 64 | } 65 | } catch { 66 | // Handle construction errors 67 | case re: RuntimeException => println("Caught Exception: " + re.getMessage) 68 | } 69 | } 70 | 71 | private def handleMetadata(key: String, value: AnyRef): Unit = { 72 | key match { 73 | case "album" => album() = value.toString 74 | case "artist" => artist() = value.toString 75 | case "title" => title() = value.toString 76 | case "year" => year() = value.toString 77 | case "image" => albumCover() = value.asInstanceOf[javafx.scene.image.Image] 78 | case _ => println("Unhandled metadata key: " + key + ", value: " + value) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #222; 3 | -fx-background-color: black; 4 | -fx-background-color: radial-gradient(center 50% 50%, radius 60%, #111, #000); 5 | -fx-background-image: url("resources/cross.png"); 6 | } 7 | 8 | .button { 9 | -fx-text-fill: #E0E0E0; 10 | } 11 | 12 | .label { 13 | -fx-font-size: 18pt; 14 | -fx-text-fill: #aaa; 15 | } 16 | 17 | #title { 18 | -fx-font-size: 24pt; 19 | -fx-text-fill: #ddd; 20 | } 21 | 22 | #openButton { 23 | -fx-graphic: url("resources/music_note.png"); 24 | } 25 | 26 | #playPauseButton, #seekEndButton, #seekStartButton { 27 | -fx-background-color: transparent; 28 | } 29 | 30 | #playPauseButton:hover, #seekEndButton:hover, #seekStartButton:hover { 31 | -fx-background-color: rgb(255, 255, 255, 0.1); 32 | } 33 | 34 | #seekEndButton { 35 | -fx-graphic: url("resources/next.png"); 36 | } 37 | 38 | #seekStartButton { 39 | -fx-graphic: url("resources/prev.png"); 40 | } 41 | 42 | #volumeHigh { 43 | -fx-image: url("resources/volHigh.png"); 44 | } 45 | 46 | #volumeLow { 47 | -fx-image: url("resources/volLow.png"); 48 | } 49 | 50 | .statusDisplay { 51 | -fx-border-color: white; 52 | -fx-border-radius: 2; 53 | -fx-border-width: 1; 54 | -fx-text-fill: white; 55 | -fx-font-size: 10pt; 56 | } 57 | 58 | .mediaText { 59 | -fx-text-fill: white; 60 | -fx-font-size: 11pt; 61 | } 62 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/defaultAlbum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/defaultAlbum.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/music_note.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/next.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/pause.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/play.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/prev.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/volHigh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/volHigh.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer3/resources/volLow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer3/resources/volLow.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/AbstractView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import scalafx.event.ActionEvent 4 | import scalafx.scene.Node 5 | 6 | /** 7 | * @author Jarek Sacha 8 | */ 9 | abstract class AbstractView[T <: Node](protected val songMadel: SongModel) { 10 | 11 | private val _viewNode: T = initView() 12 | 13 | def viewNode: T = _viewNode 14 | 15 | def onNextPageAction(nextHandler: ActionEvent => Unit): Unit = {} 16 | 17 | protected def initView(): T 18 | } 19 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/AudioPlayer4.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import com.sun.javafx.runtime as csjfxr 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.scene.input.TransferMode 8 | import scalafx.scene.layout.{BorderPane, StackPane} 9 | import scalafx.scene.{Node, Scene} 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | object AudioPlayer4 extends JFXApp3 { 15 | println("JavaFX version: " + csjfxr.VersionInfo.getRuntimeVersion) 16 | 17 | private var songModel: SongModel = _ 18 | private var playerControlsView: PlayerControlsView = _ 19 | private var metaDataView: MetadataView = _ 20 | private var equalizerView: EqualizerView = _ 21 | private var page1: Node = _ 22 | private var page2: Node = _ 23 | private var rootNode: StackPane = _ 24 | 25 | override def start(): Unit = { 26 | 27 | songModel = new SongModel() { 28 | url = "https://traffic.libsyn.com/dickwall/JavaPosse373.mp3" 29 | } 30 | 31 | page1 = createPageOne() 32 | page2 = createPageTwo() 33 | rootNode = new StackPane { 34 | children = page1 35 | } 36 | 37 | stage = new PrimaryStage { 38 | title = "Audio Player 4" 39 | scene = new Scene(rootNode, 800, 400) { 40 | val stylesheet = getClass.getResource("media.css") 41 | stylesheets += stylesheet.toString 42 | } 43 | initSceneDragAndDrop(scene()) 44 | } 45 | 46 | songModel.mediaPlayer().play() 47 | } 48 | 49 | private def createPageOne(): Node = { 50 | metaDataView = new MetadataView(songModel) 51 | playerControlsView = new PlayerControlsView(songModel) 52 | playerControlsView.onNextPageAction { _ => rootNode.children = page2 } 53 | new BorderPane { 54 | center = metaDataView.viewNode 55 | bottom = playerControlsView.viewNode 56 | } 57 | } 58 | 59 | private def createPageTwo(): Node = { 60 | equalizerView = new EqualizerView(songModel) 61 | equalizerView.onNextPageAction { _ => 62 | println("Got back button click") 63 | rootNode.children = page1 64 | } 65 | equalizerView.viewNode 66 | } 67 | 68 | private def initSceneDragAndDrop(scene: Scene): Unit = { 69 | scene.onDragOver = event => { 70 | val db = event.dragboard 71 | if (db.hasFiles || db.hasUrl) { 72 | // NOTE: We need to pass in `TransferMode` as Java vararg, since we use `javafx.scene.input.DragEvent` 73 | event.acceptTransferModes(TransferMode.Any: _*) 74 | } 75 | event.consume() 76 | } 77 | 78 | scene.onDragDropped = event => { 79 | val db = event.dragboard 80 | val url = 81 | if (db.hasFiles) { 82 | db.getFiles.get(0).toURI.toString 83 | } else if (db.hasUrl) { 84 | db.getUrl 85 | } else { 86 | null 87 | } 88 | 89 | if (url != null) { 90 | songModel.url = url 91 | songModel.mediaPlayer().play() 92 | } 93 | event.dropCompleted = (url != null) 94 | event.consume() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/EqualizerView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import scalafx.Includes.* 4 | import scalafx.event.ActionEvent 5 | import scalafx.geometry.{HPos, Insets, Orientation, VPos} 6 | import scalafx.scene.control.{Button, Label, Slider} 7 | import scalafx.scene.layout.{GridPane, Priority, RowConstraints} 8 | import scalafx.scene.media.{EqualizerBand, MediaPlayer} 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | class EqualizerView(songModel: SongModel) extends AbstractView[GridPane](songModel) { 14 | private final val StartFreq: Double = 250.0 15 | private final val BandCount: Int = 7 16 | private var spectrumBars: Array[SpectrumBar] = _ 17 | private var spectrumListener: SpectrumListener = _ 18 | private val backButton = new Button { 19 | text = "Back" 20 | id = "backButton" 21 | prefWidth = 50 22 | prefHeight = 32 23 | } 24 | 25 | songModel.mediaPlayer.onChange { 26 | (_, oldValue, _) => 27 | if (oldValue != null) { 28 | oldValue.setAudioSpectrumListener(null) 29 | clearGridPane() 30 | } 31 | 32 | createEQInterface() 33 | } 34 | 35 | createEQInterface() 36 | 37 | viewNode.scene.onChange { 38 | (_, _, newValue) => 39 | val mp = songModel.mediaPlayer() 40 | mp.audioSpectrumListener = if (newValue != null) spectrumListener else null 41 | } 42 | 43 | override def onNextPageAction(nextHandler: ActionEvent => Unit): Unit = { 44 | backButton.onAction = nextHandler 45 | } 46 | 47 | protected def initView(): GridPane = { 48 | val middle = new RowConstraints() 49 | val outside = new RowConstraints() { 50 | vgrow = Priority.Always 51 | } 52 | new GridPane { 53 | padding = Insets(10) 54 | hgap = 20 55 | rowConstraints ++= Seq(outside, middle, outside) 56 | } 57 | } 58 | 59 | private def createEQInterface(): Unit = { 60 | val gridPane = viewNode 61 | val mediaPlayer = songModel.mediaPlayer() 62 | createEQBands(gridPane, mediaPlayer) 63 | createSpectrumBars(gridPane) 64 | spectrumListener = new SpectrumListener(StartFreq, mediaPlayer, spectrumBars) 65 | GridPane.setValignment(backButton, VPos.Bottom) 66 | GridPane.setHalignment(backButton, HPos.Center) 67 | GridPane.setMargin(backButton, Insets(20, 0, 0, 0)) 68 | gridPane.add(backButton, 0, 3) 69 | } 70 | 71 | private def createEQBands(gp: GridPane, mp: MediaPlayer): Unit = { 72 | val bands = mp.getAudioEqualizer.getBands 73 | bands.clear() 74 | val min = EqualizerBand.MinGain 75 | val max = EqualizerBand.MaxGain 76 | val mid = (max - min) / 2 77 | var freq = StartFreq 78 | 79 | for (j <- 0 until BandCount) { 80 | val theta = j.toDouble / (BandCount - 1).toDouble * (2 * math.Pi) 81 | val scale = 0.4 * (1 + math.cos(theta)) 82 | val gain = min + mid + (mid * scale) 83 | bands.add(new EqualizerBand(freq, freq / 2, gain)) 84 | freq *= 2 85 | } 86 | 87 | for (i <- 0 until bands.size) { 88 | val band = bands.get(i) 89 | val slider = createEQSlider(band, min, max) 90 | val label = new Label { 91 | text = formatFrequency(band.getCenterFrequency) 92 | styleClass ++= Seq("mediaText", "eqLabel") 93 | } 94 | GridPane.setHalignment(label, HPos.Center) 95 | GridPane.setHalignment(slider, HPos.Center) 96 | GridPane.setHgrow(slider, Priority.Always) 97 | gp.add(label, i, 1) 98 | gp.add(slider, i, 2) 99 | } 100 | } 101 | 102 | private def createEQSlider(eb: EqualizerBand, minValue: Double, maxValue: Double) = new Slider { 103 | min = minValue 104 | max = maxValue 105 | value = eb.gain() 106 | styleClass += "eqSlider" 107 | orientation = Orientation.Vertical 108 | value <==> eb.gain 109 | prefWidth = 44 110 | } 111 | 112 | private def createSpectrumBars(gridPane: GridPane): Unit = { 113 | spectrumBars = new Array[SpectrumBar](BandCount) 114 | for (i <- spectrumBars.indices) { 115 | spectrumBars(i) = new SpectrumBar(100, 20) 116 | spectrumBars(i).setMaxWidth(44) 117 | GridPane.setHalignment(spectrumBars(i), HPos.Center) 118 | gridPane.add(spectrumBars(i), i, 0) 119 | } 120 | } 121 | 122 | private def formatFrequency(centerFrequency: Double): String = 123 | if (centerFrequency < 1000) { 124 | "%.0f Hz".format(centerFrequency) 125 | } else { 126 | "%.1f kHz".format(centerFrequency / 1000) 127 | } 128 | 129 | private def clearGridPane(): Unit = { 130 | viewNode.children.foreach(GridPane.clearConstraints) 131 | viewNode.children.clear() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/MetadataView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import scalafx.Includes.* 4 | import scalafx.geometry.{Insets, VPos} 5 | import scalafx.scene.control.Label 6 | import scalafx.scene.effect.Reflection 7 | import scalafx.scene.image.ImageView 8 | import scalafx.scene.layout.{ColumnConstraints, GridPane, Priority, RowConstraints} 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | class MetadataView(songModel: SongModel) extends AbstractView[GridPane](songModel) { 14 | 15 | def initView(): GridPane = { 16 | val title = new Label { 17 | text <== songModel.title 18 | id = "title" 19 | } 20 | val artist = new Label { 21 | text <== songModel.artist 22 | id = "artist" 23 | } 24 | val album = new Label { 25 | text <== songModel.album 26 | id = "album" 27 | } 28 | val year = new Label { 29 | text <== songModel.year 30 | id = "year" 31 | } 32 | val albumCover = new ImageView { 33 | image <== songModel.albumCover 34 | fitWidth = 240 35 | preserveRatio = true 36 | smooth = true 37 | effect = new Reflection { 38 | fraction = 0.2 39 | } 40 | } 41 | 42 | new GridPane { 43 | padding = Insets(10) 44 | hgap = 20 45 | add(albumCover, 0, 0, 1, GridPane.Remaining) 46 | add(title, 1, 0) 47 | add(artist, 1, 1) 48 | add(album, 1, 2) 49 | add(year, 1, 3) 50 | columnConstraints ++= Seq( 51 | new ColumnConstraints(), 52 | // NOTE: the call to delegate to avoid compilation error. 53 | // Should `scalafx.scene.layout.GridPane.columnConstraints_=()` be fixed to work without call to delegate? 54 | new ColumnConstraints { 55 | hgrow = Priority.Always 56 | }.delegate 57 | ) 58 | val r0 = new RowConstraints { 59 | valignment = VPos.Top 60 | } 61 | rowConstraints ++= Seq(r0, r0, r0, r0) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/SongModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import javafx.scene.image as jfxsi 4 | import scalafx.beans.property.{ObjectProperty, ReadOnlyObjectWrapper, StringProperty} 5 | import scalafx.collections.ObservableMap.Add 6 | import scalafx.scene.image.Image 7 | import scalafx.scene.media.{Media, MediaPlayer} 8 | 9 | /** 10 | * @author Jarek Sacha 11 | */ 12 | class SongModel { 13 | private val DefaultImageURL = classOf[SongModel].getResource("resources/defaultAlbum.png").toString 14 | private val DefaultImageCover = new Image(DefaultImageURL) 15 | val album = new StringProperty(this, "album") 16 | val artist = new StringProperty(this, "artist") 17 | val title = new StringProperty(this, "title") 18 | val year = new StringProperty(this, "year") 19 | // NOTE: use of `javafx.scene.image.Image` instead of `scalafx.scene.image.Image`, this is required for binding in 20 | // MetadataView to compile. 21 | val albumCover = new ObjectProperty[jfxsi.Image](this, "albumCover") 22 | private val _mediaPlayer = new ReadOnlyObjectWrapper[MediaPlayer](this, "mediaPlayer") 23 | 24 | resetProperties() 25 | 26 | def mediaPlayer = _mediaPlayer.readOnlyProperty 27 | 28 | def url: String = if (mediaPlayer() != null) mediaPlayer().media.source else null 29 | 30 | def url_=(url: String): Unit = { 31 | if (mediaPlayer() != null) mediaPlayer().stop() 32 | 33 | initializeMedia(url) 34 | } 35 | 36 | private def resetProperties(): Unit = { 37 | artist() = "" 38 | album() = "" 39 | title() = "" 40 | year() = "" 41 | albumCover() = DefaultImageCover 42 | } 43 | 44 | private def initializeMedia(url: String): Unit = { 45 | resetProperties() 46 | 47 | try { 48 | val media = new Media(url) { 49 | metadata.onChange((_, change) => { 50 | change match { 51 | case Add(key, added) => handleMetadata(key, added) 52 | case _ => 53 | } 54 | }) 55 | } 56 | 57 | _mediaPlayer() = new MediaPlayer(media) { 58 | self => 59 | // Handle errors during playback 60 | onError = { 61 | val errorMessage = self.media.error().getMessage 62 | println("MediaPlayer Error: " + errorMessage) 63 | } 64 | } 65 | } catch { 66 | // Handle construction errors 67 | case re: RuntimeException => println("Caught Exception: " + re.getMessage) 68 | } 69 | } 70 | 71 | private def handleMetadata(key: String, value: AnyRef): Unit = { 72 | key match { 73 | case "album" => album() = value.toString 74 | case "artist" => artist() = value.toString 75 | case "title" => title() = value.toString 76 | case "year" => year() = value.toString 77 | case "image" => albumCover() = value.asInstanceOf[javafx.scene.image.Image] 78 | case _ => println("Unhandled metadata key: " + key + ", value: " + value) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/SpectrumBar.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import javafx.scene.layout as jfxsl 4 | import scalafx.Includes.* 5 | import scalafx.geometry.Pos 6 | import scalafx.scene.shape.Rectangle 7 | 8 | /** 9 | * Custom component for displaying a spectrum bar. 10 | * 11 | * @author Jarek Sacha 12 | */ 13 | class SpectrumBar(maxValue: Int, barCount: Int) extends jfxsl.VBox { 14 | private val SPACING = 1.0 15 | private val ASPECT_RATIO = 3.0 16 | private val MIN_BAR_HEIGHT = 3.0 17 | private var lastWidth = 0.0 18 | private var lastHeight = 0.0 19 | getStyleClass.add("spectrumBar") 20 | 21 | setSpacing(SPACING) 22 | setAlignment(Pos.BottomCenter) 23 | 24 | for (i <- 0 until barCount) { 25 | val c = (i.toDouble / barCount.toDouble * 255.0).toInt 26 | val r = new Rectangle { 27 | visible = false 28 | style = s"-fx-fill: ladder( rgb($c, $c, $c), red 30%, yellow 70%, #56F32B 90%);" 29 | arcWidth = 2 30 | arcHeight = 2 31 | } 32 | 33 | getChildren.add(r) 34 | } 35 | 36 | protected override def computeMinHeight(width: Double) = computeHeight(MIN_BAR_HEIGHT) 37 | 38 | protected override def computeMinWidth(height: Double) = computeWidthForHeight(MIN_BAR_HEIGHT) 39 | 40 | protected override def computePrefHeight(width: Double) = computeHeight(5) 41 | 42 | protected override def computePrefWidth(height: Double) = computeWidthForHeight(5) 43 | 44 | def setValue(value: Double): Unit = { 45 | val barsLit = math.min(barCount, math.round(value / maxValue * barCount).toInt) 46 | val childList = getChildren 47 | for (i <- 0 until childList.size) { 48 | childList.get(i).setVisible(i > barCount - barsLit) 49 | } 50 | } 51 | 52 | protected override def layoutChildren(): Unit = { 53 | if (lastWidth != getWidth || lastHeight != getHeight) { 54 | val spacing = SPACING * (barCount - 1) 55 | val barHeight = (getHeight - getVerticalPadding - spacing) / barCount 56 | val barWidth = math.min(barHeight * ASPECT_RATIO, getWidth - getHorizontalPadding) 57 | 58 | for (node <- getChildren) { 59 | val r = node.asInstanceOf[javafx.scene.shape.Rectangle] 60 | r.width = barWidth 61 | r.height = barHeight 62 | } 63 | 64 | lastWidth = getWidth 65 | lastHeight = getHeight 66 | } 67 | 68 | super.layoutChildren() 69 | } 70 | 71 | private def computeWidthForHeight(barHeight: Double) = barHeight * ASPECT_RATIO + getHorizontalPadding 72 | 73 | private def computeHeight(barHeight: Double): Double = { 74 | val vPadding = getVerticalPadding 75 | val barHeights = barHeight * barCount 76 | val spacing = SPACING * (barCount - 1) 77 | barHeights + spacing + vPadding 78 | } 79 | 80 | private def getVerticalPadding: Double = { 81 | val padding = getPadding 82 | padding.top + padding.bottom 83 | } 84 | 85 | private def getHorizontalPadding: Double = { 86 | val padding = getPadding 87 | padding.left + padding.right 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/SpectrumListener.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.AudioPlayer4 2 | 3 | import javafx.scene.media as jfxcm 4 | import scalafx.scene.media.MediaPlayer 5 | 6 | /** 7 | * @author Jarek Sacha 8 | */ 9 | class SpectrumListener(startFreq: Double, mp: MediaPlayer, bars: Array[SpectrumBar]) 10 | extends jfxcm.AudioSpectrumListener { 11 | private val minValue = mp.audioSpectrumThreshold() 12 | private val norms = createNormArray() 13 | private val bandCount = mp.audioSpectrumNumBands() 14 | private val spectrumBucketCounts = createBucketCounts(startFreq, bandCount) 15 | 16 | def spectrumDataUpdate(timestamp: Double, duration: Double, magnitudes: Array[Float], phases: Array[Float]): Unit = { 17 | var index = 0 18 | var bucketIndex = 0 19 | var currentBucketCount = 0 20 | var sum = 0.0 21 | 22 | while (index < magnitudes.length) { 23 | sum += magnitudes(index) - minValue 24 | currentBucketCount += 1 25 | 26 | if (currentBucketCount >= spectrumBucketCounts(bucketIndex)) { 27 | bars(bucketIndex).setValue(sum / norms(bucketIndex)) 28 | currentBucketCount = 0 29 | sum = 0.0 30 | bucketIndex += 1 31 | } 32 | 33 | index += 1 34 | } 35 | } 36 | 37 | private def createNormArray(): Array[Double] = { 38 | val normArray = new Array[Double](bars.length) 39 | var currentNorm = 0.05 40 | for (i <- normArray.indices) { 41 | normArray(i) = 1 + currentNorm 42 | currentNorm *= 2 43 | } 44 | normArray 45 | } 46 | 47 | private def createBucketCounts(startFreq: Double, bandCount: Int): Array[Int] = { 48 | val bucketCounts = new Array[Int](bars.length) 49 | 50 | val bandwidth = 22050.0 / bandCount 51 | val centerFreq = bandwidth / 2 52 | var currentSpectrumFreq = centerFreq 53 | var currentEQFreq = startFreq / 2 54 | var currentCutoff = 0d 55 | var currentBucketIndex = -1 56 | 57 | for (_ <- 0 until bandCount) { 58 | if (currentSpectrumFreq > currentCutoff) { 59 | currentEQFreq *= 2 60 | currentCutoff = currentEQFreq + currentEQFreq / 2 61 | currentBucketIndex += 1 62 | } 63 | 64 | if (currentBucketIndex < bucketCounts.length) { 65 | bucketCounts(currentBucketIndex) += 1 66 | currentSpectrumFreq += bandwidth 67 | } 68 | } 69 | 70 | bucketCounts 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #222; 3 | -fx-background-color: black; 4 | -fx-background-color: radial-gradient(center 50% 50%, radius 60%, #111, #000); 5 | -fx-background-image: url("resources/cross.png"); 6 | } 7 | 8 | .button { 9 | -fx-text-fill: #E0E0E0; 10 | } 11 | 12 | .label { 13 | -fx-font-size: 18pt; 14 | -fx-text-fill: #aaa; 15 | } 16 | 17 | #title { 18 | -fx-font-size: 24pt; 19 | -fx-text-fill: #ddd; 20 | } 21 | 22 | #openButton { 23 | -fx-graphic: url("resources/music_note.png"); 24 | } 25 | 26 | #eqButton { 27 | -fx-shape: "M 1,0 L 10,0 13,5 10,10 1,10 Q 0 10 0 9 L 0,1 Q 0 0 1 0"; 28 | -fx-font-size: 12pt; 29 | -fx-alignment: center-left; 30 | } 31 | 32 | .eqSlider { 33 | -fx-background-radius: 5; 34 | -fx-background-color: #222, #888, black; 35 | -fx-background-insets: 0, 1 0 0 1, 1; 36 | -fx-padding: 2 10; 37 | } 38 | 39 | .eqSlider .thumb { 40 | -fx-background-image: url("resources/thumb.png"); 41 | -fx-padding: 12; 42 | } 43 | 44 | .eqSlider:vertical .track { 45 | -fx-padding: 5; 46 | } 47 | 48 | .spectrumBar { 49 | -fx-background-radius: 5; 50 | -fx-background-color: #222, #888, black; 51 | -fx-background-insets: 0, 1 0 0 1, 1; 52 | -fx-padding: 2 10; 53 | } 54 | 55 | #backButton { 56 | -fx-shape: "M 3,0 L 12,0 Q 13,0 13,1 L 13,9 Q 13,10 12,10 L 3,10 1,5"; 57 | -fx-font-size: 12pt; 58 | -fx-alignment: center-right; 59 | } 60 | 61 | #playPauseButton, #seekEndButton, #seekStartButton { 62 | -fx-background-color: transparent; 63 | } 64 | 65 | #playPauseButton:hover, #seekEndButton:hover, #seekStartButton:hover { 66 | -fx-background-color: rgb(255, 255, 255, 0.1); 67 | } 68 | 69 | #seekEndButton { 70 | -fx-graphic: url("resources/next.png"); 71 | } 72 | 73 | #seekStartButton { 74 | -fx-graphic: url("resources/prev.png"); 75 | } 76 | 77 | #volumeHigh { 78 | -fx-image: url("resources/volHigh.png"); 79 | } 80 | 81 | #volumeLow { 82 | -fx-image: url("resources/volLow.png"); 83 | } 84 | 85 | .statusDisplay { 86 | -fx-border-color: white; 87 | -fx-border-radius: 2; 88 | -fx-border-width: 1; 89 | -fx-text-fill: white; 90 | -fx-font-size: 10pt; 91 | } 92 | 93 | .mediaText { 94 | -fx-text-fill: white; 95 | -fx-font-size: 11pt; 96 | } 97 | 98 | .eqLabel { 99 | -fx-padding: 8 3; 100 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/defaultAlbum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/defaultAlbum.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/music_note.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/next.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/pause.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/play.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/prev.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/volHigh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/volHigh.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/AudioPlayer4/resources/volLow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/AudioPlayer4/resources/volLow.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/BasicAudioClip/BasicAudioClip.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.BasicAudioClip 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.Insets 7 | import scalafx.scene.Scene 8 | import scalafx.scene.control.Button 9 | import scalafx.scene.layout.StackPane 10 | import scalafx.scene.media.AudioClip 11 | 12 | /** 13 | * Playing an audio clip. 14 | * 15 | * @author Jarek Sacha 16 | */ 17 | object BasicAudioClip extends JFXApp3 { 18 | 19 | override def start(): Unit = { 20 | 21 | val resource = getClass.getResource("resources/beep.wav") 22 | val audioClip = new AudioClip(resource.toString) 23 | val stackPane = new StackPane { 24 | padding = Insets(10) 25 | children = new Button { 26 | text = "Bing Zzzzt!" 27 | onAction = () => audioClip.play(1.0) 28 | } 29 | } 30 | 31 | stage = new PrimaryStage { 32 | title = "Basic AudioClip Example" 33 | scene = new Scene(stackPane, 200, 200) { 34 | stylesheets += getClass.getResource("media.css").toString 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/BasicAudioClip/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #606060; 3 | -fx-background-color: radial-gradient(center 50% 50%, radius 60%, #666a6b, #2b2f32); 4 | -fx-background-image: url("resources/cross.png"); 5 | /* -fx-background-repeat: repeat; */ 6 | -fx-background-position: left top; 7 | } 8 | 9 | .button { 10 | -fx-text-fill: #E0E0E0; 11 | -fx-font-size: 20pt; 12 | -fx-pref-width: 300px; 13 | } 14 | 15 | .label { 16 | -fx-text-fill: #E0E0E0; 17 | } 18 | 19 | #clipLabel { 20 | -fx-font-size: 24pt; 21 | } 22 | 23 | .hyperlink { 24 | -fx-text-fill: #808080; 25 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/BasicAudioClip/resources/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/BasicAudioClip/resources/beep.wav -------------------------------------------------------------------------------- /src/proscalafx/ch08/BasicAudioClip/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/BasicAudioClip/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/CodeMonkeyToDo.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.CodeMonkeyToDo 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.geometry.{HPos, Insets, Pos} 7 | import scalafx.scene.Scene 8 | import scalafx.scene.control.{Button, Hyperlink, Label, Slider} 9 | import scalafx.scene.layout.{GridPane, Priority, VBox} 10 | import scalafx.scene.media.AudioClip 11 | import scalafx.scene.web.WebView 12 | 13 | /** 14 | * Controlling the playback parameters of an AudioClip. 15 | * 16 | * @author Jarek Sacha 17 | */ 18 | object CodeMonkeyToDo extends JFXApp3 { 19 | 20 | override def start(): Unit = { 21 | val coffeeClip = new AudioClip(clipResourceString("resources/coffee.mp3")) 22 | val jobClip = new AudioClip(clipResourceString("resources/job.mp3")) 23 | val meetingClip = new AudioClip(clipResourceString("resources/meeting.mp3")) 24 | 25 | val grid = new GridPane { 26 | padding = Insets(10) 27 | hgap = 10 28 | vgap = 5 29 | } 30 | 31 | createClipList(grid, coffeeClip, jobClip, meetingClip) 32 | 33 | stage = new PrimaryStage { 34 | scene = new Scene(grid, 640, 380) { 35 | title = "AudioClip Example" 36 | stylesheets += getClass.getResource("media.css").toString 37 | } 38 | } 39 | } 40 | 41 | private def clipResourceString(clipName: String): String = { 42 | getClass.getResource(clipName).toString 43 | } 44 | 45 | private def createControls(grid: GridPane): (Slider, Slider, Slider) = { 46 | val volumeLabel = new Label { 47 | text = "Volume" 48 | } 49 | val rateLabel = new Label { 50 | text = "Rate" 51 | } 52 | val balanceLabel = new Label { 53 | text = "Balance" 54 | } 55 | 56 | GridPane.setHalignment(volumeLabel, HPos.Center) 57 | GridPane.setHalignment(rateLabel, HPos.Center) 58 | GridPane.setHalignment(balanceLabel, HPos.Center) 59 | 60 | val volumeSlider = new Slider { 61 | min = 0.0 62 | max = 1.0 63 | value = 1.0 64 | hgrow = Priority.Always 65 | } 66 | val rateSlider = new Slider { 67 | min = 0.25 68 | max = 2.5 69 | value = 1.0 70 | hgrow = Priority.Always 71 | } 72 | val balanceSlider = new Slider { 73 | min = -1.0 74 | max = 1.0 75 | value = 0.0 76 | hgrow = Priority.Always 77 | } 78 | 79 | grid.add(volumeLabel, 0, 2) 80 | grid.add(volumeSlider, 0, 3) 81 | grid.add(rateLabel, 1, 2) 82 | grid.add(rateSlider, 1, 3) 83 | grid.add(balanceLabel, 2, 2) 84 | grid.add(balanceSlider, 2, 3) 85 | 86 | (volumeSlider, rateSlider, balanceSlider) 87 | } 88 | 89 | private def createClipList( 90 | grid: GridPane, 91 | coffeeClip: AudioClip, 92 | jobClip: AudioClip, 93 | meetingClip: AudioClip 94 | ): Unit = { 95 | 96 | val (volumeSlider, rateSlider, balanceSlider) = createControls(grid) 97 | 98 | val clipLabel = new Label { 99 | text = "Code Monkey To-Do List:" 100 | id = "clipLabel" 101 | } 102 | val getUpButton = new Button { 103 | text = "Get Up, Get Coffee" 104 | prefWidth = 300 105 | onAction = play(coffeeClip, volumeSlider, rateSlider, balanceSlider) 106 | } 107 | val goToJobButton: Button = new Button { 108 | text = "Go to Job" 109 | prefWidth = 300 110 | onAction = play(jobClip, volumeSlider, rateSlider, balanceSlider) 111 | } 112 | val meetingButton: Button = new Button { 113 | text = "Have Boring Meeting" 114 | prefWidth = 300 115 | onAction = play(meetingClip, volumeSlider, rateSlider, balanceSlider) 116 | } 117 | val link = new Hyperlink { 118 | text = "About Code Monkey..." 119 | onAction = () => { 120 | val webView = new WebView { 121 | engine.load("https://www.jonathancoulton.com/2006/04/14/" + "thing-a-week-29-code-monkey/") 122 | } 123 | val stage = new PrimaryStage { 124 | title = "Code Monkey" 125 | scene = new Scene(webView, 720, 480) 126 | } 127 | stage.show() 128 | } 129 | } 130 | 131 | val vbox = new VBox { 132 | spacing = 30 133 | alignment = Pos.TopCenter 134 | children ++= Seq(clipLabel, getUpButton, goToJobButton, meetingButton, link) 135 | } 136 | 137 | GridPane.setHalignment(vbox, HPos.Center) 138 | 139 | GridPane.setHgrow(vbox, Priority.Always) 140 | GridPane.setVgrow(vbox, Priority.Always) 141 | grid.add(vbox, 0, 0, GridPane.Remaining, 1) 142 | } 143 | 144 | /** Returns a function that can be assigned to `oAction` */ 145 | private def play(audioClip: AudioClip, volumeSlider: Slider, rateSlider: Slider, balanceSlider: Slider): () => Unit = 146 | () => audioClip.play(volumeSlider.value(), balanceSlider.value(), rateSlider.value(), 0.0, 0) 147 | } 148 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #606060; 3 | -fx-background-color: radial-gradient(center 50% 25%, radius 60%, #666a6b, #2b2f32); 4 | -fx-background-image: url("resources/cross.png"); 5 | /* -fx-background-repeat: repeat;*/ 6 | -fx-background-position: left top; 7 | } 8 | 9 | .button { 10 | -fx-text-fill: #E0E0E0; 11 | -fx-font-size: 20pt; 12 | -fx-pref-width: 300px; 13 | } 14 | 15 | .label { 16 | -fx-text-fill: #E0E0E0; 17 | } 18 | 19 | #clipLabel { 20 | -fx-font-size: 24pt; 21 | } 22 | 23 | .hyperlink { 24 | -fx-text-fill: #808080; 25 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/coffee.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/coffee.mp3 -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/coffee.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/coffee.wav -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/job.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/job.mp3 -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/job.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/job.wav -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/meeting.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/meeting.mp3 -------------------------------------------------------------------------------- /src/proscalafx/ch08/CodeMonkeyToDo/resources/meeting.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/CodeMonkeyToDo/resources/meeting.wav -------------------------------------------------------------------------------- /src/proscalafx/ch08/FullScreenVideoPlayer/FullScreenVideoPlayer.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.FullScreenVideoPlayer 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.scene.Scene 6 | import scalafx.scene.layout.StackPane 7 | import scalafx.scene.media.{Media, MediaPlayer, MediaView} 8 | 9 | import java.io.File 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | object FullScreenVideoPlayer extends JFXApp3 { 15 | 16 | override def start(): Unit = { 17 | 18 | val file = new File("media/omgrobots.mp4") 19 | val media = new Media(file.toURI.toString) 20 | val mediaPlayer = new MediaPlayer(media) 21 | val mediaView = new MediaView(mediaPlayer) { 22 | fitWidth <== scene.selectDouble("width") 23 | fitHeight <== scene.selectDouble("height") 24 | preserveRatio = true 25 | } 26 | 27 | val root = new StackPane { 28 | children = mediaView 29 | } 30 | 31 | stage = new PrimaryStage { 32 | title = "Video Player 1" 33 | fullScreen = true 34 | scene = new Scene(root, 960, 540) 35 | } 36 | 37 | mediaPlayer.play() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer1/VideoPlayer1.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer1 2 | 3 | import scalafx.application.JFXApp3 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.scene.Scene 6 | import scalafx.scene.layout.StackPane 7 | import scalafx.scene.media.{Media, MediaPlayer, MediaView} 8 | 9 | import java.io.File 10 | 11 | /** 12 | * @author Jarek Sacha 13 | */ 14 | object VideoPlayer1 extends JFXApp3 { 15 | 16 | override def start(): Unit = { 17 | val file = new File("media/omgrobots.mp4") 18 | 19 | val media = new Media(file.toURI.toString) 20 | val mediaPlayer = new MediaPlayer(media) 21 | val mediaView = new MediaView(mediaPlayer) 22 | val root = new StackPane { 23 | children = mediaView 24 | } 25 | 26 | stage = new PrimaryStage { 27 | title = "Video Player 1" 28 | scene = new Scene(root, 960, 540) 29 | } 30 | 31 | mediaPlayer.play() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer2/VideoPlayer2.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer2 2 | 3 | import scalafx.Includes.* 4 | import scalafx.application.JFXApp3.PrimaryStage 5 | import scalafx.application.{JFXApp3, Platform} 6 | import scalafx.geometry.Pos 7 | import scalafx.scene.Scene 8 | import scalafx.scene.control.Label 9 | import scalafx.scene.layout.StackPane 10 | import scalafx.scene.media.{Media, MediaPlayer, MediaView} 11 | import scalafx.util.Duration 12 | 13 | import java.io.File 14 | import java.net.URL 15 | import scala.language.postfixOps 16 | 17 | /** 18 | * @author Jarek Sacha 19 | */ 20 | object VideoPlayer2 extends JFXApp3 { 21 | 22 | override def start(): Unit = { 23 | 24 | val markerText = new Label { 25 | alignmentInParent = Pos.TopCenter 26 | } 27 | 28 | val file = new File("media/omgrobots.mp4") 29 | val media = new Media(file.toURI.toString) { 30 | markers ++= Map( 31 | "Robot Finds Wall" -> (3100 ms), 32 | "Then Finds the Green Line" -> (5600 ms), 33 | "Robot Grabs Sled" -> (8000 ms), 34 | "And Heads for Home" -> (11500 ms) 35 | ) 36 | } 37 | 38 | val mediaPlayer = new MediaPlayer(media) { 39 | onMarker = event => 40 | Platform.runLater { 41 | markerText.text = event.marker.getKey 42 | } 43 | } 44 | 45 | val mediaView = new MediaView(mediaPlayer) 46 | val root = new StackPane { 47 | children ++= Seq(mediaView, markerText) 48 | onMouseClicked = () => { 49 | mediaPlayer.seek(Duration.Zero) 50 | markerText.text = "" 51 | } 52 | } 53 | 54 | stage = new PrimaryStage { 55 | title = "Video Player 2" 56 | scene = new Scene(root, 960, 540) { 57 | val stylesheet: URL = getClass.getResource("media.css") 58 | stylesheets += stylesheet.toString 59 | } 60 | } 61 | 62 | mediaPlayer.play() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer2/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-background-color: black; 3 | } 4 | 5 | .label { 6 | -fx-padding: 20; 7 | -fx-font-size: 36pt; 8 | -fx-text-fill: white; 9 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer3/VideoPlayer3.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer3 2 | 3 | import scalafx.Includes.* 4 | import scalafx.animation.{ParallelTransition, TranslateTransition} 5 | import scalafx.application.JFXApp3.PrimaryStage 6 | import scalafx.application.{JFXApp3, Platform} 7 | import scalafx.geometry.{Pos, Rectangle2D} 8 | import scalafx.scene.control.Label 9 | import scalafx.scene.layout.StackPane 10 | import scalafx.scene.media.{Media, MediaPlayer, MediaView} 11 | import scalafx.scene.{Node, Scene} 12 | import scalafx.util.Duration 13 | 14 | import java.io.File 15 | import java.net.URL 16 | import scala.language.postfixOps 17 | 18 | /** 19 | * @author Jarek Sacha 20 | */ 21 | object VideoPlayer3 extends JFXApp3 { 22 | 23 | override def start(): Unit = { 24 | 25 | val message = new Label { 26 | text = "I \u2764 Robots" 27 | visible = false 28 | } 29 | 30 | val file = new File("media/omgrobots.mp4") 31 | val media = new Media(file.toURI.toString) { 32 | markers ++= Map( 33 | "Split" -> (3000 ms), 34 | "Join" -> (9000 ms) 35 | ) 36 | } 37 | 38 | val mediaPlayer = new MediaPlayer(media) 39 | 40 | val mediaView1 = new MediaView(mediaPlayer) { 41 | viewport = new Rectangle2D(0, 0, 960 / 2, 540) 42 | alignmentInParent = Pos.CenterLeft 43 | } 44 | 45 | val mediaView2 = new MediaView(mediaPlayer) { 46 | viewport = new Rectangle2D(960 / 2, 0, 960 / 2, 540) 47 | alignmentInParent = Pos.CenterRight 48 | } 49 | 50 | val root = new StackPane { 51 | children ++= Seq(message, mediaView1, mediaView2) 52 | onMouseClicked = () => { 53 | mediaPlayer.seek(Duration.Zero) 54 | message.visible = false 55 | } 56 | } 57 | 58 | stage = new PrimaryStage { 59 | title = "Video Player 3" 60 | scene = new Scene(root, 960, 540) { 61 | val stylesheet: URL = getClass.getResource("media.css") 62 | stylesheets += stylesheet.toString 63 | } 64 | } 65 | 66 | mediaPlayer.onMarker = event => 67 | Platform.runLater { 68 | event.marker.getKey match { 69 | case "Split" => 70 | message.visible = true 71 | buildSplitTransition(mediaView1, mediaView2).play() 72 | case _ => buildJoinTransition(mediaView1, mediaView2).play() 73 | } 74 | } 75 | 76 | mediaPlayer.play() 77 | } 78 | 79 | private def buildJoinTransition(one: Node, two: Node) = new ParallelTransition { 80 | children = List( 81 | new TranslateTransition { 82 | duration = (1000 ms) 83 | node = one 84 | byX = 200 85 | }, 86 | new TranslateTransition { 87 | duration = (1000 ms) 88 | node = two 89 | byX = -200 90 | } 91 | ) 92 | } 93 | 94 | private def buildSplitTransition(one: Node, two: Node) = new ParallelTransition { 95 | children = List( 96 | new TranslateTransition { 97 | duration = (1000 ms) 98 | node = one 99 | byX = -200 100 | }, 101 | new TranslateTransition { 102 | duration = (1000 ms) 103 | node = two 104 | byX = 200 105 | } 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer3/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-background-color: black; 3 | } 4 | 5 | .label { 6 | -fx-padding: 20; 7 | -fx-font-size: 48pt; 8 | -fx-text-fill: darkred; 9 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/AbstractView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import scalafx.event.ActionEvent 4 | import scalafx.scene.Node 5 | 6 | /** 7 | * @author Jarek Sacha 8 | */ 9 | abstract class AbstractView[T <: Node](protected val mediaModel: MediaModel) { 10 | 11 | private val _viewNode: T = initView() 12 | 13 | def viewNode: T = _viewNode 14 | 15 | def onNextPageAction(nextHandler: ActionEvent => Unit): Unit = {} 16 | 17 | protected def initView(): T 18 | } 19 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/EqualizerView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import scalafx.Includes.* 4 | import scalafx.event.ActionEvent 5 | import scalafx.geometry.{HPos, Insets, Orientation, VPos} 6 | import scalafx.scene.control.{Button, Label, Slider} 7 | import scalafx.scene.layout.{GridPane, Priority, RowConstraints} 8 | import scalafx.scene.media.{EqualizerBand, MediaPlayer} 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | class EqualizerView(mediaModel: MediaModel) extends AbstractView[GridPane](mediaModel) { 14 | 15 | private final val StartFrequency: Double = 250.0 16 | private final val BandCount: Int = 7 17 | private var spectrumBars: Array[SpectrumBar] = _ 18 | private var spectrumListener: SpectrumListener = _ 19 | private val backButton = new Button { 20 | text = "Back" 21 | id = "backButton" 22 | prefWidth = 50 23 | prefHeight = 32 24 | } 25 | 26 | mediaModel.mediaPlayer.onChange { 27 | (_, oldValue, _) => 28 | if (oldValue != null) { 29 | oldValue.setAudioSpectrumListener(null) 30 | clearGridPane() 31 | } 32 | 33 | createEQInterface() 34 | } 35 | 36 | createEQInterface() 37 | 38 | viewNode.scene.onChange { 39 | (_, _, newValue) => 40 | val mp = mediaModel.mediaPlayer() 41 | mp.audioSpectrumListener = if (newValue != null) spectrumListener else null 42 | } 43 | 44 | override def onNextPageAction(nextHandler: ActionEvent => Unit): Unit = { 45 | backButton.onAction = nextHandler 46 | } 47 | 48 | protected def initView(): GridPane = { 49 | val middle = new RowConstraints() 50 | val outside = new RowConstraints() { 51 | vgrow = Priority.Always 52 | } 53 | new GridPane { 54 | padding = Insets(10) 55 | hgap = 20 56 | rowConstraints ++= Seq(outside, middle, outside) 57 | } 58 | } 59 | 60 | private def createEQInterface(): Unit = { 61 | val gridPane = viewNode 62 | val mediaPlayer = mediaModel.mediaPlayer() 63 | 64 | createEQBands(gridPane, mediaPlayer) 65 | createSpectrumBars(gridPane) 66 | spectrumListener = new SpectrumListener(StartFrequency, mediaPlayer, spectrumBars) 67 | 68 | GridPane.setValignment(backButton, VPos.Bottom) 69 | GridPane.setHalignment(backButton, HPos.Center) 70 | GridPane.setMargin(backButton, Insets(20, 0, 0, 0)) 71 | gridPane.add(backButton, 0, 3) 72 | } 73 | 74 | private def createEQBands(gp: GridPane, mp: MediaPlayer): Unit = { 75 | val bands = mp.getAudioEqualizer.getBands 76 | 77 | bands.clear() 78 | 79 | val min = EqualizerBand.MinGain 80 | val max = EqualizerBand.MaxGain 81 | val mid = (max - min) / 2 82 | var freq = StartFrequency 83 | 84 | // Create the equalizer bands with the gains preset to 85 | // a nice cosine wave pattern. 86 | for (j <- 0 until BandCount) { 87 | // Use j and BandCount to calculate a value between 0 and 2*pi 88 | val theta = j.toDouble / (BandCount - 1).toDouble * (2 * math.Pi) 89 | 90 | // The cos function calculates a scale value between 0 and 0.4 91 | val scale = 0.4 * (1 + math.cos(theta)) 92 | 93 | // Set the gain to be a value between the midpoint and 0.9*max. 94 | val gain = min + mid + (mid * scale) 95 | 96 | bands.add(new EqualizerBand(freq, freq / 2, gain)) 97 | freq *= 2 98 | } 99 | 100 | for (i <- 0 until bands.size) { 101 | val band = bands.get(i) 102 | val slider = createEQSlider(band, min, max) 103 | 104 | val label = new Label { 105 | text = formatFrequency(band.getCenterFrequency) 106 | styleClass ++= Seq("mediaText", "eqLabel") 107 | } 108 | 109 | GridPane.setHalignment(label, HPos.Center) 110 | GridPane.setHalignment(slider, HPos.Center) 111 | GridPane.setHgrow(slider, Priority.Always) 112 | 113 | gp.add(label, i, 1) 114 | gp.add(slider, i, 2) 115 | } 116 | } 117 | 118 | private def createEQSlider(eb: EqualizerBand, minValue: Double, maxValue: Double) = new Slider { 119 | min = minValue 120 | max = maxValue 121 | value = eb.gain() 122 | styleClass += "eqSlider" 123 | orientation = Orientation.Vertical 124 | value <==> eb.gain 125 | prefWidth = 44 126 | } 127 | 128 | private def createSpectrumBars(gridPane: GridPane): Unit = { 129 | spectrumBars = new Array[SpectrumBar](BandCount) 130 | 131 | for (i <- spectrumBars.indices) { 132 | spectrumBars(i) = new SpectrumBar(100, 20) 133 | spectrumBars(i).setMaxWidth(44) 134 | GridPane.setHalignment(spectrumBars(i), HPos.Center) 135 | gridPane.add(spectrumBars(i), i, 0) 136 | } 137 | } 138 | 139 | private def formatFrequency(centerFrequency: Double): String = 140 | if (centerFrequency < 1000) { 141 | "%.0f Hz".format(centerFrequency) 142 | } else { 143 | "%.1f kHz".format(centerFrequency / 1000) 144 | } 145 | 146 | private def clearGridPane(): Unit = { 147 | viewNode.children.foreach(GridPane.clearConstraints) 148 | viewNode.children.clear() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/MediaModel.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import javafx.scene.{image as jfxsi, media as jfxsm} 4 | import scalafx.Includes.* 5 | import scalafx.beans.property.{ObjectProperty, ReadOnlyObjectWrapper, StringProperty} 6 | import scalafx.collections.ObservableMap.Add 7 | import scalafx.scene.image.Image 8 | import scalafx.scene.media.{Media, MediaPlayer} 9 | 10 | /** 11 | * @author Jarek Sacha 12 | */ 13 | class MediaModel { 14 | private val DefaultImageURL = classOf[MediaModel].getResource("resources/defaultAlbum.png").toString 15 | private val DefaultImageCover = new Image(DefaultImageURL) 16 | 17 | val album = new StringProperty(this, "album") 18 | val artist = new StringProperty(this, "artist") 19 | val title = new StringProperty(this, "title") 20 | val year = new StringProperty(this, "year") 21 | 22 | // NOTE: use of `javafx.scene.image.Image` instead of `scalafx.scene.image.Image`, this is required for binding in 23 | // MetadataView to compile. 24 | val albumCover = new ObjectProperty[jfxsi.Image](this, "albumCover") 25 | 26 | // NOTE We use jfxsm.MediaPlayer as type parameter to ReadOnlyObjectWrapper so we can bind to MediaView.mediaPlayer 27 | // see proscalafx.ch08.VideoPlayer4.VideoView.initView 28 | private val _mediaPlayer = new ReadOnlyObjectWrapper[jfxsm.MediaPlayer](this, "mediaPlayer") 29 | 30 | resetProperties() 31 | 32 | def mediaPlayer = _mediaPlayer.readOnlyProperty 33 | 34 | def url: String = if (mediaPlayer() != null) mediaPlayer().media.source else null 35 | 36 | def url_=(url: String): Unit = { 37 | if (mediaPlayer() != null) mediaPlayer().stop() 38 | 39 | initializeMedia(url) 40 | } 41 | 42 | private def resetProperties(): Unit = { 43 | artist() = "" 44 | album() = "" 45 | title() = "" 46 | year() = "" 47 | albumCover() = DefaultImageCover 48 | } 49 | 50 | private def initializeMedia(url: String): Unit = { 51 | resetProperties() 52 | 53 | try { 54 | val media = new Media(url) { 55 | metadata.onChange((_, change) => { 56 | change match { 57 | case Add(key, added) => handleMetadata(key, added) 58 | case _ => 59 | } 60 | }) 61 | } 62 | 63 | _mediaPlayer() = new MediaPlayer(media) { 64 | self => 65 | // Handle errors during playback 66 | onError = { 67 | val errorMessage = self.media.error().getMessage 68 | println("MediaPlayer Error: " + errorMessage) 69 | } 70 | }.delegate 71 | } catch { 72 | // Handle construction errors 73 | case re: RuntimeException => println("Caught Exception: " + re.getMessage) 74 | } 75 | } 76 | 77 | private def handleMetadata(key: String, value: AnyRef): Unit = { 78 | key match { 79 | case "album" => album() = value.toString 80 | case "artist" => artist() = value.toString 81 | case "title" => title() = value.toString 82 | case "year" => year() = value.toString 83 | case "image" => albumCover() = value.asInstanceOf[javafx.scene.image.Image] 84 | case _ => println("Unhandled metadata key: " + key + ", value: " + value) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/SpectrumBar.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import javafx.scene.layout as jfxsl 4 | import scalafx.Includes.* 5 | import scalafx.geometry.Pos 6 | import scalafx.scene.shape.Rectangle 7 | 8 | /** 9 | * Custom component for displaying a spectrum bar. 10 | * 11 | * @author Jarek Sacha 12 | */ 13 | class SpectrumBar(maxValue: Int, barCount: Int) extends jfxsl.VBox { 14 | private val SPACING = 1.0 15 | private val ASPECT_RATIO = 3.0 16 | private val MIN_BAR_HEIGHT = 3.0 17 | private var lastWidth = 0.0 18 | private var lastHeight = 0.0 19 | getStyleClass.add("spectrumBar") 20 | 21 | setSpacing(SPACING) 22 | setAlignment(Pos.BottomCenter) 23 | 24 | for (i <- 0 until barCount) { 25 | val c = (i.toDouble / barCount.toDouble * 255.0).toInt 26 | val r = new Rectangle { 27 | visible = false 28 | style = s"-fx-fill: ladder( rgb($c, $c, $c), red 30%, yellow 70%, #56F32B 90%);" 29 | arcWidth = 2 30 | arcHeight = 2 31 | } 32 | 33 | getChildren.add(r) 34 | } 35 | 36 | protected override def computeMinHeight(width: Double) = computeHeight(MIN_BAR_HEIGHT) 37 | 38 | protected override def computeMinWidth(height: Double) = computeWidthForHeight(MIN_BAR_HEIGHT) 39 | 40 | protected override def computePrefHeight(width: Double) = computeHeight(5) 41 | 42 | protected override def computePrefWidth(height: Double) = computeWidthForHeight(5) 43 | 44 | def setValue(value: Double): Unit = { 45 | val barsLit = math.min(barCount, math.round(value / maxValue * barCount).toInt) 46 | val childList = getChildren 47 | for (i <- 0 until childList.size) { 48 | childList.get(i).visible = (i > barCount - barsLit) 49 | } 50 | } 51 | 52 | protected override def layoutChildren(): Unit = { 53 | if (lastWidth != getWidth || lastHeight != getHeight) { 54 | val spacing = SPACING * (barCount - 1) 55 | val barHeight = (getHeight - getVerticalPadding - spacing) / barCount 56 | val barWidth = math.min(barHeight * ASPECT_RATIO, getWidth - getHorizontalPadding) 57 | 58 | for (node <- getChildren) { 59 | val r = node.asInstanceOf[javafx.scene.shape.Rectangle] 60 | r.width = barWidth 61 | r.height = barHeight 62 | } 63 | 64 | lastWidth = getWidth 65 | lastHeight = getHeight 66 | } 67 | 68 | super.layoutChildren() 69 | } 70 | 71 | private def computeWidthForHeight(barHeight: Double) = barHeight * ASPECT_RATIO + getHorizontalPadding 72 | 73 | private def computeHeight(barHeight: Double): Double = { 74 | val vPadding = getVerticalPadding 75 | val barHeights = barHeight * barCount 76 | val spacing = SPACING * (barCount - 1) 77 | barHeights + spacing + vPadding 78 | } 79 | 80 | private def getVerticalPadding: Double = { 81 | val padding = getPadding 82 | padding.top + padding.bottom 83 | } 84 | 85 | private def getHorizontalPadding: Double = { 86 | val padding = getPadding 87 | padding.left + padding.right 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/SpectrumListener.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import javafx.scene.media as jfxcm 4 | import scalafx.scene.media.MediaPlayer 5 | 6 | /** 7 | * @author Jarek Sacha 8 | */ 9 | class SpectrumListener(startFreq: Double, mp: MediaPlayer, bars: Array[SpectrumBar]) 10 | extends jfxcm.AudioSpectrumListener { 11 | private val minValue = mp.audioSpectrumThreshold() 12 | private val norms = createNormArray() 13 | private val bandCount = mp.audioSpectrumNumBands() 14 | private val spectrumBucketCounts = createBucketCounts(startFreq, bandCount) 15 | 16 | def spectrumDataUpdate(timestamp: Double, duration: Double, magnitudes: Array[Float], phases: Array[Float]): Unit = { 17 | var index = 0 18 | var bucketIndex = 0 19 | var currentBucketCount = 0 20 | var sum = 0.0 21 | 22 | while (index < magnitudes.length) { 23 | sum += magnitudes(index) - minValue 24 | currentBucketCount += 1 25 | 26 | if (currentBucketCount >= spectrumBucketCounts(bucketIndex)) { 27 | bars(bucketIndex).setValue(sum / norms(bucketIndex)) 28 | currentBucketCount = 0 29 | sum = 0.0 30 | bucketIndex += 1 31 | } 32 | 33 | index += 1 34 | } 35 | } 36 | 37 | private def createNormArray(): Array[Double] = { 38 | val normArray = new Array[Double](bars.length) 39 | var currentNorm = 0.05 40 | for (i <- normArray.indices) { 41 | normArray(i) = 1 + currentNorm 42 | currentNorm *= 2 43 | } 44 | normArray 45 | } 46 | 47 | private def createBucketCounts(startFreq: Double, bandCount: Int): Array[Int] = { 48 | val bucketCounts = new Array[Int](bars.length) 49 | 50 | val bandwidth = 22050.0 / bandCount 51 | val centerFreq = bandwidth / 2 52 | var currentSpectrumFreq = centerFreq 53 | var currentEQFreq = startFreq / 2 54 | var currentCutoff = 0d 55 | var currentBucketIndex = -1 56 | 57 | for (_ <- 0 until bandCount) { 58 | if (currentSpectrumFreq > currentCutoff) { 59 | currentEQFreq *= 2 60 | currentCutoff = currentEQFreq + currentEQFreq / 2 61 | currentBucketIndex += 1 62 | } 63 | 64 | if (currentBucketIndex < bucketCounts.length) { 65 | bucketCounts(currentBucketIndex) += 1 66 | currentSpectrumFreq += bandwidth 67 | } 68 | } 69 | 70 | bucketCounts 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/VideoPlayer4.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import scalafx.Includes.* 4 | import scalafx.scene.input.TransferMode 5 | import scalafx.scene.layout.{BorderPane, StackPane} 6 | import scalafx.scene.{Node, Scene} 7 | 8 | /** 9 | * @author Jarek Sacha 10 | */ 11 | class VideoPlayer4 { 12 | 13 | private var playerControlsView: PlayerControlsView = _ 14 | private var videoView: VideoView = _ 15 | private var equalizerView: EqualizerView = _ 16 | 17 | val mediaModel: MediaModel = new MediaModel() { 18 | url = "https://download.oracle.com/otndocs/products/javafx/oow2010-2.mp4" 19 | } 20 | 21 | private val page1 = createPageOne() 22 | private val page2 = createPageTwo() 23 | val rootNode: StackPane = new StackPane { 24 | children = page1 25 | } 26 | 27 | private def createPageOne(): Node = { 28 | videoView = new VideoView(mediaModel) 29 | playerControlsView = new PlayerControlsView(mediaModel) 30 | playerControlsView.onNextPageAction { _ => rootNode.children = page2 } 31 | new BorderPane { 32 | center = videoView.viewNode 33 | bottom = playerControlsView.viewNode 34 | } 35 | } 36 | 37 | private def createPageTwo(): Node = { 38 | equalizerView = new EqualizerView(mediaModel) 39 | equalizerView.onNextPageAction { _ => rootNode.children = page1 } 40 | equalizerView.viewNode 41 | } 42 | 43 | def initSceneDragAndDrop(scene: Scene): Unit = { 44 | scene.onDragOver = event => { 45 | val db = event.dragboard 46 | if (db.hasFiles || db.hasUrl) { 47 | // NOTE: We need to pass in `TransferMode` as Java vararg, since we use `javafx.scene.input.DragEvent` 48 | event.acceptTransferModes(TransferMode.Any: _*) 49 | } 50 | event.consume() 51 | } 52 | 53 | scene.onDragDropped = event => { 54 | val db = event.dragboard 55 | val url = 56 | if (db.hasFiles) { 57 | db.getFiles.get(0).toURI.toString 58 | } else if (db.hasUrl) { 59 | db.getUrl 60 | } else { 61 | null 62 | } 63 | 64 | if (url != null) { 65 | mediaModel.url = url 66 | mediaModel.mediaPlayer().play() 67 | } 68 | event.dropCompleted = (url != null) 69 | event.consume() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/VideoPlayer4App.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import com.sun.javafx.runtime as csjfxr 4 | import scalafx.Includes.* 5 | import scalafx.application.JFXApp3 6 | import scalafx.application.JFXApp3.PrimaryStage 7 | import scalafx.scene.Scene 8 | 9 | /** 10 | * @author Jarek Sacha 11 | */ 12 | object VideoPlayer4App extends JFXApp3 { 13 | 14 | override def start(): Unit = { 15 | 16 | println("JavaFX version: " + csjfxr.VersionInfo.getRuntimeVersion) 17 | 18 | val videoPlayer = new VideoPlayer4() 19 | 20 | val stylesheet = getClass.getResource("media.css") 21 | 22 | stage = new PrimaryStage { 23 | title = "Video Player 4" 24 | scene = new Scene(videoPlayer.rootNode, 1024, 680) { 25 | stylesheets += stylesheet.toString 26 | } 27 | videoPlayer.initSceneDragAndDrop(scene()) 28 | } 29 | 30 | videoPlayer.mediaModel.mediaPlayer().play() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/VideoView.scala: -------------------------------------------------------------------------------- 1 | package proscalafx.ch08.VideoPlayer4 2 | 3 | import scalafx.scene.media.MediaView 4 | 5 | /** 6 | * @author Jarek Sacha 7 | */ 8 | class VideoView(mediaModel: MediaModel) extends AbstractView[MediaView](mediaModel) { 9 | 10 | protected def initView(): MediaView = { 11 | val mediaView = new MediaView 12 | mediaView.mediaPlayer <== mediaModel.mediaPlayer 13 | mediaView 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/media.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: #222; 3 | -fx-background-color: black; 4 | -fx-background-color: radial-gradient(center 50% 50%, radius 60%, #111, #000); 5 | -fx-background-image: url("resources/cross.png"); 6 | } 7 | 8 | .button { 9 | -fx-text-fill: #E0E0E0; 10 | } 11 | 12 | .label { 13 | -fx-font-size: 18pt; 14 | -fx-text-fill: #aaa; 15 | } 16 | 17 | #title { 18 | -fx-font-size: 24pt; 19 | -fx-text-fill: #ddd; 20 | } 21 | 22 | #openButton { 23 | -fx-graphic: url("resources/filmstrip.png"); 24 | } 25 | 26 | #eqButton { 27 | -fx-shape: "M 1,0 L 10,0 13,5 10,10 1,10 Q 0 10 0 9 L 0,1 Q 0 0 1 0"; 28 | -fx-font-size: 12pt; 29 | -fx-alignment: center-left; 30 | } 31 | 32 | .eqSlider { 33 | -fx-background-radius: 5; 34 | -fx-background-color: #222, #888, black; 35 | -fx-background-insets: 0, 1 0 0 1, 1; 36 | -fx-padding: 2 10; 37 | } 38 | 39 | .eqSlider .thumb { 40 | -fx-background-image: url("resources/thumb.png"); 41 | -fx-padding: 12; 42 | } 43 | 44 | .eqSlider:vertical .track { 45 | -fx-padding: 5; 46 | } 47 | 48 | .spectrumBar { 49 | -fx-background-radius: 5; 50 | -fx-background-color: #222, #888, black; 51 | -fx-background-insets: 0, 1 0 0 1, 1; 52 | -fx-padding: 2 10; 53 | } 54 | 55 | #backButton { 56 | -fx-shape: "M 3,0 L 12,0 Q 13,0 13,1 L 13,9 Q 13,10 12,10 L 3,10 1,5"; 57 | -fx-font-size: 12pt; 58 | -fx-alignment: center-right; 59 | } 60 | 61 | #playPauseButton, #seekEndButton, #seekStartButton { 62 | -fx-background-color: transparent; 63 | } 64 | 65 | #playPauseButton:hover, #seekEndButton:hover, #seekStartButton:hover { 66 | -fx-background-color: rgb(255, 255, 255, 0.1); 67 | } 68 | 69 | #seekEndButton { 70 | -fx-graphic: url("resources/next.png"); 71 | } 72 | 73 | #seekStartButton { 74 | -fx-graphic: url("resources/prev.png"); 75 | } 76 | 77 | #volumeHigh { 78 | -fx-image: url("resources/volHigh.png"); 79 | } 80 | 81 | #volumeLow { 82 | -fx-image: url("resources/volLow.png"); 83 | } 84 | 85 | .statusDisplay { 86 | -fx-border-color: white; 87 | -fx-border-radius: 2; 88 | -fx-border-width: 1; 89 | -fx-text-fill: white; 90 | -fx-font-size: 10pt; 91 | } 92 | 93 | .mediaText { 94 | -fx-text-fill: white; 95 | -fx-font-size: 11pt; 96 | } 97 | 98 | .eqLabel { 99 | -fx-padding: 8 3; 100 | } -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/PlayPause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/PlayPause.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/cross.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/defaultAlbum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/defaultAlbum.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/filmstrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/filmstrip.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/music_note.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/next.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/pause.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/play.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/prev.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/thumb.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/volHigh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/volHigh.png -------------------------------------------------------------------------------- /src/proscalafx/ch08/VideoPlayer4/resources/volLow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalafx/ProScalaFX/5fd9c6559a9f07d46bdae6a97fe001bd6bc20d42/src/proscalafx/ch08/VideoPlayer4/resources/volLow.png -------------------------------------------------------------------------------- /src/proscalafx/ch10/fxml/AdoptionForm.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |