An RSS Reader written with Scala and JavaFX (the beginning)

I just started writing an RSS Reader application using JavaFX and Scala, and I thought I’d post the initial code here. This code shows several advanced Scala techniques that Scala developers might need to use when writing Scala code to interact with Java, and in this case, JavaFX.

Without any further ado, this is what the application looks like when it’s running:

An RSS Reader written with Scala and JavaFX

And this is the Scala and JavaFX source code that corresponds to that image:

package com.alvinalexander.rssreader

import javafx.scene.layout.GridPane
import javafx.geometry.Insets
import javafx.scene.text.Text
import javafx.scene.text.Font
import javafx.scene.text.FontWeight
import javafx.scene.image.ImageView
import javafx.scene.image.Image
import javafx.geometry.VPos
import javafx.application.Application
import java.io.IOException
import javafx.stage.Stage
import javafx.scene.Group
import javafx.scene.Scene
import javafx.application.Application
import javafx.application.Platform
import javafx.scene.Group
import javafx.scene.Scene
import javafx.scene.control.ListView
import javafx.scene.layout.BorderPane
import javafx.scene.web.WebEngine
import javafx.scene.web.WebHistory
import javafx.scene.web.WebView
import javafx.stage.Screen
import javafx.stage.Stage
import javafx.beans.value.ChangeListener
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.geometry.Rectangle2D
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
import java.io.BufferedReader
import java.io.FileReader
import javafx.scene.control.ScrollPane
import javafx.scene.layout.HBox

object JavaFxRssReaderUsingBorderPane {
    def main(args: Array[String]) {
        Application.launch(classOf[JavaFxRssReaderUsingBorderPane], args: _*)
    }
}

class JavaFxRssReaderUsingBorderPane extends Application {

    @Override
    @throws(classOf[IOException])
    def start(primaryStage: Stage) {

        val rootGroup = new Group
        val scene = new Scene(rootGroup, initialWidth, initialHeight)
        
        // COL 1: RSS FEEDS (LIST VIEW)
        val rssFeeds = getRssFeeds
        val rssFeedsListView: ListView[String]  = new ListView[String]()
        val rssFeedsData: ObservableList[String] = FXCollections.observableArrayList(rssFeeds: _*)
        rssFeedsListView.setItems(rssFeedsData)
        rssFeedsListView.setPrefWidth(150)

        // COL 2: (LIST VIEW)
        val col2ListView: ListView[String]  = new ListView[String]()
        val col2Data: ObservableList[String] = FXCollections.observableArrayList(fakeDataForColumn2: _*)
        col2ListView.setItems(col2Data)
        col2ListView.setPrefWidth(150)

        // COL 3: WEB VIEW
        val webView = new WebView
        val webEngine = webView.getEngine
        
        // SCROLL PANES
        val rssFeedsScrollPane = new ScrollPane(rssFeedsListView)
        val col2ScrollPane = new ScrollPane(col2ListView)

        // HBOX 
        val hbox = new HBox
        hbox.setPadding(new Insets(15, 12, 15, 12))
        hbox.setSpacing(10)
        hbox.getChildren.addAll(rssFeedsScrollPane, col2ScrollPane)

        // HANDLE SIZING AND CLICKS
        handleInitialSizing(webView, rssFeedsListView, col2ListView)
        handleResizing(scene, webView, rssFeedsListView, col2ListView)
        handleUserClickingRssFeed(webEngine, rssFeedsListView)

        // PUT THE LAYOUT TOGETHER
        val borderPane = new BorderPane
        borderPane.setLeft(hbox)
        borderPane.setCenter(webView)
        rootGroup.getChildren.add(borderPane)

        // START THE APP
        primaryStage.setTitle("Al’s RSS Reader")
        primaryStage.setScene(scene)
        primaryStage.show()

    }
    
    def handleUserClickingRssFeed(webEngine: WebEngine,
                                  rssFeedsListView: ListView[String])
    {
        // ChangeListener solution found at: http://hohonuuli.blogspot.com/2012/10/example-of-javafx-changelistener-in.html
        rssFeedsListView.getSelectionModel.selectedItemProperty.addListener(
            new ChangeListener[String] {
                def changed(ov: ObservableValue[_ <: String], oldValue: String, newValue: String) {
                    webEngine.load(newValue)
            }
        })
    }

    def handleInitialSizing(webView: WebView,
                            rssFeedsListView: ListView[String],
                            col2ListView: ListView[String])
    {
        rssFeedsListView.setPrefHeight(initialHeight)
        col2ListView.setPrefHeight(initialHeight)
        webView.setPrefHeight(initialHeight)
        webView.setPrefWidth(initialWidth)
    }

    def handleResizing(scene: Scene, 
                       webView: WebView,
                       rssFeedsListView: ListView[String],
                       col2ListView: ListView[String])
    {
        // https://blog.idrsolutions.com/2012/11/adding-a-window-resize-listener-to-javafx-scene/
        scene.widthProperty.addListener(new ChangeListener[Number]() {
            @Override def changed(observableValue: ObservableValue[_ <: Number], oldSceneWidth: Number, newSceneWidth: Number) {
                webView.setPrefWidth(newSceneWidth.doubleValue*0.9)
            }
        })

        scene.heightProperty.addListener(new ChangeListener[Number]() {
            @Override def changed(observableValue: ObservableValue[_ <: Number], oldSceneHeight: Number, newSceneHeight: Number) {
                val newHeight = newSceneHeight.doubleValue*0.98
                webView.setPrefHeight(newHeight)
                rssFeedsListView.setPrefHeight(newHeight)
                col2ListView.setPrefHeight(newHeight)
            }
        })
    }
    
    def initialHeight = getScreenBounds.getHeight*3/4
    def initialWidth = getScreenBounds.getWidth*3/4
    def getScreenBounds: Rectangle2D = Screen.getPrimary.getVisualBounds
    
    def fakeDataForColumn2: Array[String] = {
        Array(
            "Fake Story 1", 
            "Fake Story 2",
            "Fake Story 3"
        )
    }

    //TODO best way to handle this? haskell would wrap it in an IO.
    def getRssFeeds: Array[String] = {
        val usersHomeDir = System.getProperty("user.home")
        val canonDataFile = usersHomeDir + "/" + "RssReader.data"
        readFileToStringArray(canonDataFile)
    }

    @throws(classOf[IOException])
    def readFileToStringArray(canonFilename: String): Array[String] = {
        val bufferedReader = new BufferedReader(new FileReader(canonFilename))
        val lines = new ArrayBuffer[String]()
        var line: String = null
        while ({line = bufferedReader.readLine; line != null}) {
            lines.add(line)
        }
        bufferedReader.close
        lines.toArray
    }

}

As the image (and source code) shows, I currently just have the data for the second JavaFX ListView mocked up. What should really happen is that when you click an RSS Feed in the first column, it should load the stories for that feed in the second column, but I have a little ways to go before I get that working.

At some point I may switch to using ScalaFX rather than JavaFX, but at the moment I’m using this hybrid approach. (I don’t know JavaFX very well, and the ScalaFX documentation is a little sparse at the moment, so I feel more comfortable doing this.)