TweetWallFX – Screencast 3

Here is the third screencast to present TweetWallFX that I’m currently developing. The application has few evolutions and is still under development.

Advertisements

TweetWallFX – Screencast 2

Here is the second screencast to present TweetWallFX that I’m currently developing. The application has few evolutions and is still under development.

TweetWallFX – Part 5

In TweetWallFX – Part 2 we talked about the wall creation. In TweetWallFX – Part 4 we had a demonstration of the chart API provided in JavaFX. Now we’re going to put everything together so that when we create a wall, the statistics tab will be created and updated in real time. This is pretty straight forward to do because we already have every needed piece to make it work.

Oh my! It works

Remember our TweetWallFXController class that was creating our wall. We had the startTweetWall() method that was creating a tab containing the wall. Remember also I was writing that we just have to make a task that every x seconds should look for new tweets. So basically we’re going to write this task, and it will create the wall and the stats.

public class TweetRefresher extends TimerTask {
  private List<Tweet> pendingTweets;
  private TweetDAO tweetDAO;
  private WallController wallController;
  private StatisticsController statsController;

  public TweetRefresher(WallController wall, StatisticsController stats) {
    this.wallController = wall;
    this.statsController = stats;
    this.tweetDAO = new FooTweetDAO();
    this.pendingTweets = new ArrayList<Tweet>();
  }

  @Override
  public void run() {
    Platform.runLater(new Runnable() {

    @Override
    public void run() {

      int nbTweetsToRetrieve = 10;
      pendingTweets.addAll(tweetDAO.getTweetForHashtag(hashtag, nbTweetsToRetrieve));

      if (pendingTweets != null && !pendingTweets.isEmpty()) {                  
        this.wallController.addTweet(pendingTweets.get(0));
        this.statsController.addTweet(pendingTweets.get(0));
        pendingTweets.remove(0);
      }
    }
  });
}

This is pretty easy to understand. We will now update our startTweetWall() method.

@FXML private void startTweetWall(ActionEvent event) {
  Tab wallTab = null, statsTab = null;
  WallController wallController = null;
  StatisticsController statsController = null;

  try {
    FXMLLoader fxmlLoader = new FXMLLoader(FXMLLibrary.getFXMLUrl(FXMLLibrary.WALL_TAB_FXML));
    wallTab = (Tab) fxmlLoader.load();
    wallController = (WallController) fxmlLoader.getController();

    // Loading the stats
    fxmlLoader.setLocation(FXMLLibrary.getFXMLUrl(FXMLLibrary.STATISTICS_TAB_FXML));
    statsTab = (Tab) fxmlLoader.load();
    statsController = (StatisticsController) fxmlLoader.getController();
  } catch (IOException ex) {
    Logger.getLogger(TweetWallFXController.class.getName()).log(Level.SEVERE, null, ex);
  }

  if (wallTab != null && statsTab != null) {
    wallTab.setText(String.format("#%1$s", this.hashtagTextField.getText()));
    statsTab.setText(String.format("Stats #%1$s", this.hashtagTextField.getText()));

    this.tabPane.getTabs().add(wallTab);
    // Also adding the stats tab
    this.tabPane.getTabs().add(statsTab);

    // Create and schedule the task
    TweetRefresher task = new TweetRefresher();
    Timer timer = new Timer();
    timer.schedule(task, 0, 5000);
  }
}

That’s it, the wall and the stats will be updated every 5 seconds. To summarize, some screenshots:



Conclusion

In this eerie of articles I hope I could give a quick overview of JavaFX. But you can do much more with it, like playing sounds and movies, display data into really rich tables and so on. And if you have some designer skills (definitively not like me) you can do pretty beautiful UI. And with JavaFX 2.2 there’s more to come! Like I said in previous articles, I really think that JFX is an amazing and promising technology and that every Java developer should have a look at it. I also would like to share really few links about JFX:

I will also continue to post some articles about JavaFX capabilities, and believe it or not, there’s a lot to talk about!

TweetWallFX – Part 4

JavaFX comes along with a highly customizable and powerful chart API out of the box: bar chart, pie chart, area chart and much more. And this API also provides you event handler to provide a high user experience allowing end-users to interact with the charts. Maybe you already know JFreeChart which still is a reference in the Java SE world but also used in some other third party libraries for Java EE. You can see the JavaFX Chart API as a really nice evolution of JFreeChart: powerful, highly customizable and out of the box!

TweetWallFX uses this API in order to display some statistics about the number of tweets captured by the wall. Typically it displays the number of tweets by hour with two main functions:

  • display the number of tweets while having the mouse over a symbol on the chart;
  • display the number of tweets by minutes when clicking on a chart symbol.

In this article we’ll focus on displaying the tweets by hour, customize our chart and define a little “mouse over” interaction.

Shall we chart?

It begins to be cleared that we’ll use a FXML file for the view and a controller. So let’s start with the FXML, named statistics-controller.fxml. As you will see, the root element is a Tab because we’ll display our chart in a specific tab, coming along with a wall.

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.collections.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.chart.*?>
<?import javafx.scene.chart.XYChart.* ?>
<?import javafx.util.converter.* ?>
<?import com.twasyl.tweetwallfx.util.converter.* ?>

<Tab xmlns:fx="http://javafx.com/fxml" fx:controller="com.twasyl.tweetwallfx.controllers.StatisticsController" closable="true">
  <content>
    <HBox style="-fx-background-color: linear-gradient(#5C5C5C 0%, #2E2E2E 100%);" alignment="CENTER" minWidth="1024" minHeight="768">
      <LineChart fx:id="chart" prefWidth="1010" prefHeight="700" legendVisible="false">
        <xAxis>
          <CategoryAxis label="Time">
            <categories>
              <FXCollections fx:factory="observableArrayList" />
            </categories>
          </CategoryAxis>
        </xAxis>
        <yAxis>
          <NumberAxis label="Number of tweets" tickUnit="10" minorTickVisible="false">
            <tickLabelFormatter>
              <NoDecimalConverter />
            </tickLabelFormatter>
          </NumberAxis>
        </yAxis>
        <data>
          <FXCollections fx:factory="observableArrayList">
            <XYChart.Series>
              <data>
                <FXCollections fx:factory="observableArrayList" />
              </data>
            </XYChart.Series>
          </FXCollections>
        </data>
      </LineChart>
    </HBox>
  </content>
</Tab>

I’ll focus on the chart creation, which is here a line chart with only one serie. As you can see I declare my <xAxis>…</xAxis> which represents the hours where tweets have been generated. It is a CategoryAxis because here, the value will be a string like 20h. The values for this axis are stored in a list, created here using <FXCollections … />. The Y axis is an axis which values are a number, so a NumberAxis. You can notice the <tickLabelFormatter /> that is a class that formats the value on the Y axis. Typically the one used won’t display decimals and looks like this:

public class NoDecimalConverter extends StringConverter<Number> {

  @Override
  public String toString(Number arg0) {
    return String.format("%1$s", arg0.intValue());
  }

  @Override
  public Number fromString(String arg0) {
    return Integer.parseInt(arg0);
  }
}

Like the axises we create a serie for our values, which are also stored inside a list created using the FXCollections factory. Before creating the controller, we will create a CSS file, statistics-style.css, in order to customize the look of our chart:

.axis {
  -fx-text-fill: white;
  -fx-tick-label-fill: white;
}
.axis-label {
  -fx-text-fill: white;
}
.chart-title {
  -fx-text-fill: white;
}
.chart-line-symbol {
  -fx-background-color: radial-gradient(radius 100%, deepskyblue, dodgerblue, royalblue);
  -fx-shape: "M197.223,82.571c-5.611,0.958-13.75-0.038-18.061-1.832c8.96-0.742,15.027-4.815,17.365-10.343 c-3.229,1.988-13.26,4.153-18.793,2.09c-0.275-1.301-0.575-2.539-0.879-3.66c-4.213-15.506-18.663-28.001-33.794-26.491 c1.22-0.495,2.458-0.955,3.708-1.375c1.656-0.597,11.436-2.192,9.898-5.639c-1.303-3.041-13.249,2.287-15.496,2.987 c2.969-1.113,7.881-3.033,8.403-6.451c-4.546,0.623-9.012,2.777-12.46,5.906c1.248-1.34,2.191-2.974,2.391-4.737 c-12.133,7.762-19.222,23.396-24.955,38.574c-4.501-4.372-8.502-7.812-12.079-9.729c-10.043-5.384-22.057-11.012-40.906-18.019 c-0.58,6.243,3.083,14.55,13.63,20.066c-2.283-0.309-6.462,0.383-9.799,1.177c1.359,7.16,5.805,13.052,17.851,15.898 c-5.503,0.362-8.353,1.624-10.928,4.318c2.506,4.978,8.63,10.831,19.628,9.627c-12.24,5.282-4.992,15.061,4.968,13.601 c-16.98,17.566-43.761,16.263-59.139,1.583c40.145,54.762,127.421,32.382,140.421-20.363 C187.951,89.841,193.672,86.386,197.223,82.571z";
  -fx-scale-shape: false;
  -fx-scale-x: 0.2;
  -fx-scale-y: 0.2;
}
.chart-series-line { -fx-stroke: #69B4E4; }

In this file we redefine the color of the text for the labels, the chart title, the color of the lines and, the most interesting part, the line symbol. You can see the -fx-shape attribute that has a “strange” value. This value is simply the SVG path drawing a twitter bird! Not kidding! JavaFX supports natively SVG! You can use it like here in CSS, or in your FXML using the SVGPath object. And to conclude with the UI part, we’ll create a FXML file, tooltip.fxml, to display a tooltip when having the mouse over a line symbol:

<HBox xmlns:fx="http://javafx.com/fxml" fx:id="pane" spacing="10" fx:controller="com.twasyl.tweetwallfx.controllers.TooltipController"
        style="-fx-background-color: linear-gradient(beige 0%, khaki 100%); -fx-background-radius: 10,10,10,10; -fx-padding: 5,5,5,5">
  <children>
    <Label fx:id="text" />
  </children>
</HBox>

Business class

In order to represent statistics in a scalable way, we will create a statistics class:

public class TweetStatistics implements Serializable {
  private IntegerProperty yearProperty;
  private IntegerProperty monthProperty;
  private IntegerProperty dayOfMonthProperty;
  private IntegerProperty hourProperty;
  private MapProperty<Integer, Integer> minutesProperty;
  private ReadOnlyIntegerProperty totalTweetsProperty;
    
  public TweetStatistics() {
    this.yearProperty = new SimpleIntegerProperty(0);
    this.monthProperty = new SimpleIntegerProperty(0);
    this.dayOfMonthProperty = new SimpleIntegerProperty(0);
    this.hourProperty = new SimpleIntegerProperty(0);
    this.minutesProperty = new SimpleMapProperty<Integer, Integer>();
    this.totalTweetsProperty = new SimpleIntegerProperty(0);
    
    this.minutesProperty.addListener(new MapChangeListener<Integer, Integer>() {
      @Override
      public void onChanged(Change<? extends Integer, ? extends Integer> event) {
        int count = 0;
        for(Entry<Integer, Integer> entry : TweetStatistics.this.minutesProperty().get().entrySet()) {
          count += entry.getValue();
        }
        ((SimpleIntegerProperty) TweetStatistics.this.totalTweetsProperty).set(count);
      }
    });
  }

  public TweetStatistics(int year, int month, int dayOfMonth,int hour, ObservableMap<Integer, Integer> minutes) {
    this();
    setYear(year);
    setMonth(month);
    setDayOfMonth(dayOfMonth);
    setHour(hour);
    setMinutes(minutes);
  }
    
  // Getter & setter
    
  public boolean tweetMatchesStatistics(Tweet tweet) {
    boolean result = false;
    if(tweet != null && tweet.getPostDate() != null) {
      int year = tweet.getPostDate().get(Calendar.YEAR);
      int month = tweet.getPostDate().get(Calendar.MONTH);
      int dom = tweet.getPostDate().get(Calendar.DAY_OF_MONTH);
      int hour = tweet.getPostDate().get(Calendar.HOUR_OF_DAY);
     
      result = year == getYear() && month == getMonth() &&
              dom == getDayOfMonth() && hour == getHour();
    }
    return result;
  }
  
  /**
   * Appends the tweet to the stats. This method calls tweetMatchesStatistics
   * @param tweet 
   */
  public boolean appendTweet(Tweet tweet) {
    boolean added = false;
    
    if(tweetMatchesStatistics(tweet)) {
      int number = 1;
      if(getMinutes().containsKey(tweet.getPostDate().get(Calendar.MINUTE))) {
        number = getMinutes().get(tweet.getPostDate().get(Calendar.MINUTE)) + 1;
      }
      getMinutes().put(tweet.getPostDate().get(Calendar.MINUTE), number);
      added = true;
    }
    return added;
  }
}

Controllers

We’ll start with the easiest one: the tooltip controller which has nothing to explain:

public class TooltipController {
  @FXML private Pane pane;
  @FXML private Label text;

  public Pane getPane() {
    return pane;
  }

  public Label getText() {
    return text;
  }
}

The StatisticsController is going to be a little bit more complex. Because the controller implements the Initializable interface, let’s see the initilize method:

public class StatisticsController implements Initializable {
  @FXML
  private LineChart<String, Integer> chart;
  private ObservableList<TweetStatistics> statistics;

  // ...
  @Override
  public void initialize(URL arg0, ResourceBundle arg1) {
    this.chart.getStylesheets().add(getClass().getResource("/com/twasyl/tweetwallfx/css/statistics-style.css").toExternalForm());
    this.statistics = FXCollections.observableArrayList();
    this.statistics.addListener(new StatisticsChangeListener());
  }
  // ...
}

You can see how to apply a CSS file to a Node, here our chart. We’re also adding a listener to our list of statistics in order to handle changes into hit. This listener is the following inner-class:

private class StatisticsChangeListener implements ListChangeListener<TweetStatistics> {
  @Override
  public void onChanged(Change<? extends TweetStatistics> event) {
    // We retrieve the one and only serie defined in the chart.
    // The serie's been created in the FXML file
    XYChart.Series<String, Integer> series = StatisticsController.this.chart.getData().get(0);
    boolean dataExists;
    short index;

   // Many events could have occurred, this is the reason of this loop
    while(event.next()) {
      // We get the sublist that contains modifications
      // We get the list this way in order not to care
      // if it was additions or removals
      List<TweetStatistics> modifications = (List<TweetStatistics>) event.getList().subList(event.getFrom(), event.getTo());

      // For each modifications, we look if the data already exists and need an update.
      // If the data doesn't exist, add it
      for(TweetStatistics ts : modifications) {
        index = 0;
        dataExists = false;
        while(!dataExists && index < series.getData().size()) {
          dataExists = series.getData().get(index).getXValue().equals(ts.getHour() + "h");
          index++;
        }

        // If the data does not exist, create it
        if(!dataExists) {
          // The third argument of the constructor for a Data is the
          // extra value
          Data<String, Integer> data = new Data<String, Integer>(
                                String.format("%1$sh", ts.getHour()), 
                                ts.getTotalTweets(), ts);
          series.getData().add(data);
          data.getNode().addEventHandler(EventType.ROOT, new ChartDataEventHandler(data));
        } else {
          Data<String, Integer> data = series.getData().get(index-1);
          data.setYValue(ts.getTotalTweets());
        }
      }
    }
  }
}

So far so good. Now another inner-class, the ChartDataEventHandler class that will handle the mouse over a line symbol of the chart, as well as the click. We’ll just cover the mouse over event.

private class ChartDataEventHandler implements EventHandler<Event> {
  Data data;
  TooltipController controller;

  public ChartDataEventHandler(Data data) {
    this.data = data;
  }

  @Override
  public void handle(Event event) {
    if (event instanceof MouseEvent) {
      MouseEvent mouseEvent = (MouseEvent) event;

      // When the mouse enter the line symbol, we have to create
      // and display the tooltip
      if (event.getEventType().equals(MouseEvent.MOUSE_ENTERED)) {
        setController();

        // If the controller of the tooltip doesn't exist, create and display it
        if (controller != null) {
          controller.getText().setText("Number of tweets: " + data.getYValue());
          double x = mouseEvent.getSceneX() + 5;
          double y = mouseEvent.getSceneY() + 5;

          controller.getPane().autosize();
          controller.getPane().setLayoutX(x);
          controller.getPane().setLayoutY(y);
          ((Pane) data.getNode().getScene().getRoot()).getChildren().add(controller.getPane());
        }
      } else if (event.getEventType().equals(MouseEvent.MOUSE_EXITED)) {
        // When the mouse exits the line symbol, remove the tooltip
        if (controller != null) {
          ((Pane) data.getNode().getScene().getRoot()).getChildren().remove(controller.getPane());
          controller = null;
        }
      } else if (event.getEventType().equals(MouseEvent.MOUSE_MOVED)) {
        // When the mouse moves over the line symbol,
        // move the tooltip
        if (controller != null) {
          double x = mouseEvent.getSceneX() + 5;
          double y = mouseEvent.getSceneY() + 5;
          controller.getPane().setLayoutX(x);
          controller.getPane().setLayoutY(y);
        }
      } else if (event.getEventType().equals(MouseEvent.MOUSE_CLICKED)) {
        // Manage the click if you want
      }
    }
  }

  public void setController() {
    FXMLLoader fxml = new FXMLLoader(FXMLLibrary.getFXMLUrl(FXMLLibrary.TOOLTIP_FXML), ResourceBundleLibrary.getApplicationPropertiesRB());
    try {
      fxml.load();
      controller = (TooltipController) fxml.getController();
    } catch (IOException ex) {
      Logger.getLogger(StatisticsController.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

And finally a method that will trigger the changes of the chart:

public void addTweet(Tweet tweet) {
  if(tweet != null) {
    boolean done = false;
    short index = 0;

    while(index < this.statistics.size() && !done) {
      done = this.statistics.get(index).appendTweet(tweet);
      if(done) {
        // Just to fire a change in the list
        this.statistics.set(index, this.statistics.get(index));
      }
      index++;
    }
    if(!done) {
      TweetStatistics ts = new TweetStatistics(tweet.getPostDate().get(Calendar.YEAR),
                        tweet.getPostDate().get(Calendar.MONTH),
                        tweet.getPostDate().get(Calendar.DAY_OF_MONTH), 
                        tweet.getPostDate().get(Calendar.HOUR_OF_DAY), 
                        null);
      ts.appendTweet(tweet);
      this.statistics.add(ts);
    }
  }
}

The final result gives you this:
Well this is it for this long and technical post.

TweetWallFX – Part 3

Hi,

Comparing to the two first parts, this article is going to propose some tips as well as some ideas that maybe could be interesting to see in JavaFX (JavaFX team members, keep reading). Well it seems to be cleared that since I took a look int JavaFX at Devoxx France I became a true fan (JavaFX team members: you simply rock by creating this version 2)! And when you start looking at it, just be honest, it’s an amazing technology! I’ll be talking in this article about transitions and FXMLLoader. Those topics are used in TweetWallFX to simplify some developments.

Transitions and animations

Since JavaFX 1 it is possible to create some transitions, animations on Node: fade in/out effects, sequential transition, rotations, scale, … and very complex ones! JFX is kind of open. Let’s take an example of fade in/out animation:

Label label = new Label("Fade me in");
// Create a transition that will apply on the label and during 1 second
FadeTransition fadeIn = new FadeTransition(new Duration(1000), label);
fadeIn.setFromValue(0);
fadeIn.setToValue(1.0);
// Go!
fadeIn.play();

label.setText("Fade me out");
FadeTransition fadeOut = new FadeTransition(new Duration(1000), label);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0);
fadeOut.play();

Well, the fade in and out code are the same except the From and To values. So let’s make it more scalable and in the way to be some “utils” methods.

public static void fadeOut(Node node, double duration) {
  getFadeOutAnimation(node, duration).play();
}

public static void fadeIn(Node node, double duration) {
  getFadeInAnimation(node, duration).play();
}

public static FadeTransition getFadeInAnimation(Node node, double duration) {
  return getFadeAnimation(node, duration, node.getOpacity(), 1.0);
}

public static FadeTransition getFadeOutAnimation(Node node, double duration) {
  return getFadeAnimation(node, duration, node.getOpacity(), 0.0);
}

private static FadeTransition getFadeAnimation(Node node, double duration, double fromValue, double toValue) {
  FadeTransition fadeTransition = new FadeTransition(new Duration(duration),
                node);
  fadeTransition.setFromValue(fromValue);
  fadeTransition.setToValue(toValue);
  return fadeTransition;
}

Easy right? As you can see, the fromValue is the current opacity of the Node because the FadeTransition is updating this value while the transition is played. So instead of asking everytime the from value, we just take the current opacity, which seems logical.
Now on the same basis we will create a simple transition which will translate a Node horizontally and/or vertically.

/** We will move the node from the given number of horizontal and vertical pixels */
public static void move(Node node, double duration, double numberOfHorizontalPixels, double numberOfVerticalPixels) {
  getMoveAnimation(node, duration, numberOfHorizontalPixels, numberOfVerticalPixels).play();
}

public static Timeline getMoveAnimation(Node node, double duration, double numberOfHorizontalPixels, double numberOfVerticalPixels) {
  KeyValue horizontalMove = null, verticalMove = null;

  if (numberOfHorizontalPixels != 0) {
    // This represents the final horizontal position of the Node:
    // Current position + "deltaX"
    horizontalMove = new KeyValue(node.layoutXProperty(),
                     node.getLayoutX() + numberOfHorizontalPixels);
  }
  if (numberOfVerticalPixels != 0) {
    // This represents the final vertical position of the Node
    // Current position + "deltaY"
    verticalMove = new KeyValue(node.layoutYProperty(),
                   node.getLayoutY() + numberOfVerticalPixels);
  }
  // KeyFrame are like the interpolation frames in Flash ...
  KeyFrame kf = new KeyFrame(new Duration(duration), horizontalMove, verticalMove);
  // The timeline will play every given KeyFrame
  Timeline timeline = new Timeline(kf);
  // Play the animation once
  timeline.setCycleCount(1);

  return timeline;
}

If you are familiar with Flash development, you may recognize some concepts here. The timeline is globally the same as the timeline in Flash. The KeyFrame is a developer defined position of an object, defined by KeyValues. In Flash, you will do an interpolation, and the last image of this interpolation is defined by you, developer. And the intermediate images (frames) are managed and computed by the engine. In JavaFX, same concept. If you’re more interested in the animations, there’re some others that you can read on the JavaFX page.

FXMLLoader

FXML files are loaded using a FXMLLoader. You have two main ways of loading such files. The first one is using the static load() method which will return you the root component of your FXML file.

// I don't manage exceptions in this example
URL url = getClass().getResource("/some/package/yourFile.fxml");
Parent root = FXMLLoader.load(url);

This is useful when you don’t really need the controller. The second way is to create an instance of FXMLLoader that allows you to, of course, get the root element of your file, as well as the controller.

// I don't manage exceptions in this example
URL url = getClass().getResource("/some/package/yourFile.fxml");
FXMLLoader loader = new FXMLLoader(url);
Object root = loader.load();
Object controller = loader.getController();

Pretty straight forward. I have an idea for maybe future version of JavaFX (if any JavaFX team members reading, this one’s for you). I don’t know if it would be possible to create like a FXML library. Indeed, I looked the sample apps provided with JavaFX and I saw that the FXML is in the same package as the controller for example, or more generally, there is a package for a functionality. I pretty like this approach, kind of well organizing your architecture. But I usually like to hava same kind of elements in the same place, for example a package for all of my FXML files, another one for my controllers and so on. I really think it is the way you like to organize your code that matters. But anyway, in TweetWallFX a lot of FXML files are there and I said to myself “Well would be great if I had some kind of FXML library”. And I created a really basic one:

public class FXMLLibrary {
  private static final String BASE_PACKAGE = "/com/twasyl/tweetwallfx/fxmls/";
  public static final String TOOLTIP_FXML = BASE_PACKAGE + "tooltip.fxml";
  public static final String TWEETWALLFX_FXML = BASE_PACKAGE + "TweetWallFX.fxml";
  public static final String TWEETBUBBLE_FXML = BASE_PACKAGE + "TweetBubble.fxml";
  public static final String STATISTICS_TAB_FXML = BASE_PACKAGE + "statistics-tab.fxml";
  public static final String STATISTICS_LOOKUP_FXML = BASE_PACKAGE + "statistics-lookup.fxml";
  public static final String WALL_TAB_FXML = BASE_PACKAGE + "wall-tab.fxml";
  public static final String APPLICATION_MENUBAR_FXML = BASE_PACKAGE + "application-menubar.fxml";
  public static final String MUSIC_PLAYER_FXML = BASE_PACKAGE + "music-player.fxml";
  
  public static URL getFXMLUrl(String fxml) {
    return FXMLLibrary.class.getResource(fxml);
  }
}

So if I need to use a particular FXML file in different places, I just need to update the URL in one place if needed. And I am wondering if such a library could be provided by default. I’m coming from the JSF world and in JSF apps you have the faces-config.xml file which contain some context-param, resource bundles and so on. Maybe in JavaFX we could have a jfx-config.xml file with some properties (like resource bundles in JSF) with some maven ideas also, like this:

<jfx-config>
  <properties>
    <base-package>/com/twasyl/tweetwallfx/fxmls</base-package>
  </properties>
  <fxmls>
    <fxml>
      <name>TOOLTIP_FXML</name>
      <url>${base-package}/tooltip.fxml</url>
      <bundle>/com/twasyl/tweetwallfx/resources/i18n/tooltip</bundle>
    </fxml>
    <fxml>
      // ...
    </fxml>
  </fxmls>
</jfx-config>

And the FXMLLoader could be based on the names provided in the config file. I don’t know if this is relevant, I just got the idea. Well that’s it for this article, stay tuned.

TweetWallFX – Part 2

Hi,

In my TweetWallFX – Part 1 article I presented to you how you could simply create a start screen with some effects available in JavaFX. In this article we’re going to see how to create the wall of tweets itself. We will not use any Twitter API but generate random tweets in order to focus on JavaFX.

“Business” part

Before to deal with the UI we are going to create some business classes on which the application will be built on. First of all we create a class representing a tweet in order not to be dependent by any Twitter API you may use.

public class Tweet implements Serializable {
  private Calendar postDate;
  private String userName, firstName, lastName;
  private String content;
  // Be careful to use the javafx.scene.image.Image
  private Image avatar;

  public Tweet() {}

  public Tweet(String userName, String firstName, 
               String lastName, String content, 
               Image avatar, Calendar postDate) {

    this.userName = userName;
    this.firstName = firstName;
    this.lastName = lastName;
    this.content = content;
    this.avatar = avatar;
    this.postDate = postDate;
  }

  // Create getters & setters
}

Next we create a simple Twitter DAO in order to provide some services, like getting some tweets. As I usually like to do, I create an interface representing my DAO, and then some implementation. This allows me to change the implementation used at any time without having to change and check my whole code. The interface will be:

public interface TweetDAO extends Serializable {
  List<Tweet> getTweetForHashtag(String hashTag, int maxResult);
}

And my foo implementation is:

public class FooTweetDAO implements TweetDAO {

  @Override
  public List<Tweet> getTweetForHashtag(String hashTag, int maxResult) {
    List<Tweet> tweets = new ArrayList<Tweet>(0);
    Tweet tweet;
    Random random = new Random();

    for(short nb = 0; nb < maxResult; nb++) {
      tweet = new Tweet("jdoe", "John", "doe", null, null, Calendar.getInstance());
      tweet.setContent("My random tweet with #" + hashTag + "-" + random.nextInt(1000));
      tweet.setAvatar(new Image(getClass().getResourceAsStream("/com/twasyl/tweetwallfx/resources/images/tweetEgg.png")));
      tweets.add(tweet);
    }
    return tweets;
  }
}

UI time

Now we’re able to get some “tweets” we will create the UI and some logic code in order to update the wall. Remember I said that I think that with JavaFX it’s the first time you can really implement the MVC design pattern in Java desktop apps. When you come from the Swing world like me, maybe you put everything of your UI in a class, instantiate manually everything and so on. But if you come from the JSF world like me too, you easily separate your view from the backing code. The first times you create some JFX apps, you may be nostalgic and want to do everything by hand, like in the old times (yeah I know this), or maybe you’re just used to do it. My advice for you is to ask yourself this question: “Do I really NEED to do everything all by myself for the UI?”. It will be hard the first times, but you’re getting better everyday! After this little introduction, let’s get back to work! I want my wall to be filled by “bubbles”, and each bubble will contain the information about a tweet. I have two main possibilities for creating this bubble:

  • Old fashioned way: create everything in the Java code by creating a wonderful class having a Label, a picture, and so on.
  • The way you’re always dreamt about: create an external file that will contain that UI element

Of course we will use the second way and create our bubble using FXML. So here we go:

<HBox xmlns:fx="http://javafx.com/fxml" fx:id="tweetBox" 
    prefHeight="50" prefWidth="500" spacing="10"
    fx:controller="com.twasyl.tweetwallfx.controllers.TweetBubbleController"
    style="-fx-background-color: linear-gradient(white 0%, gray 100%); -fx-background-radius: 10,10,10,10; -fx-padding: 5,5,5,5">
  <children>
    <ImageView id="avatarImageView" fx:id="avatarImageView" />
    <VBox spacing="10">
      <HBox spacing="10">
        <Label id="nameLabel" fx:id="nameLabel" />
        <Label id="userNameLabel" fx:id="userNameLabel" />
      </HBox>
      <Label id="tweetLabel" fx:id="tweetLabel" />
    </VBox>
  </children>
</HBox>

This code gives you a Node like this when it’s filled:

The FXML is pretty simple and everything should be known by you if you read the previous article. Nothing more, nothing less. Then we will create an FXML that will be our wall and contain some bubbles.

<Tab xmlns:fx="http://javafx.com/fxml" id="wallTab" fx:id="wallTab" 
    fx:controller="com.twasyl.tweetwallfx.controllers.WallController"
    closable="true">

  <content>
    <AnchorPane id="wallPane" fx:id="wallPane" style="-fx-background-color: linear-gradient(#5C5C5C 0%, #2E2E2E 100%);" minWidth="1024" minHeight="768">
      <children>
      </children>
    </AnchorPane>
  </content>
</Tab>

As you can see, the root element isn’t a “regular” Pane but a Tab. Indeed I decided to create a tab-based app. So here, instead of creating a Pane that will be added in a Tab in my backing code, I directly create a Tab. This Tab only contains an AnchorPane which is going to contain our bubbles and that has a linear gradient with some gray colors. You should also be familiar with the fx:controller attribute that defines a backing class as controller for the FXML.

Controller time

For now we’re already done with the UI part! (yeah that was quick) We have to create our controllers and you will see that is pretty straight forward to do. The most simple one is the TweetBubbleController so let’s take a look at it.

public class TweetBubbleController implements Initializable {
  // Some default sizes
  public static final int DEFAULT_WIDTH = 500;
  public static final int DEFAULT_HEIGHT = 80;
  public static final double DEFAULT_AVATAR_PICTURE_SIZE = 50;
  private Tweet tweet;

  // Binding with the FXML
  @FXML private ImageView avatarImageView;
  @FXML private Label tweetLabel;
  @FXML private Label nameLabel;
  @FXML private Label userNameLabel;

  public TweetBubbleController() {}

  public boolean isEmpty() {
    return getContent() == null;
  }

  public void setContent(Object content) {
    if (content instanceof Tweet) {
      this.tweet = (Tweet) content;

      // Getting the greater size: height or width
      // We don't want the pic to be greater than the bubble height
      double maxDim = Math.max(tweet.getAvatar().getWidth(), tweet.getAvatar().getHeight());

      // Setting the image of the ImageView
      this.avatarImageView.setImage(this.tweet.getAvatar());
      this.avatarImageView.setPreserveRatio(true);
      this.avatarImageView.setSmooth(true);
      this.avatarImageView.setCache(true);

      if (maxDim == tweet.getAvatar().getHeight()) {
        this.avatarImageView.setFitHeight(DEFAULT_AVATAR_PICTURE_SIZE);
      } else {
        this.avatarImageView.setFitWidth(DEFAULT_AVATAR_PICTURE_SIZE);
      }

      // Updating other labels
      this.nameLabel.setText(String.format("%1$s %2$s", this.tweet.getFirstName(), this.tweet.getLastName().toUpperCase()));
      this.userNameLabel.setText(String.format("@%1$s", this.tweet.getUserName()));
      
      this.tweetLabel.setText(this.tweet.getContent());
      this.tweetLabel.setWrapText(true);
      this.tweetLabel.setMaxWidth(DEFAULT_WIDTH);
      this.tweetLabel.setMaxHeight(DEFAULT_HEIGHT);
    }
  }

  public Object getContent() {
    return this.tweet;
  }

  @Override
  public void initialize(URL arg0, ResourceBundle arg1) {}
}

Now the most tricky one but not really difficult: the WallController that updates the wall:

public class WallController implements Initializable {

  private double HORIZONTAL_SPACE_BETWEEN_TWEETS = 5;
  private double VERTICAL_SPACE_BETWEEN_TWEETS = 10;
  private double WIDTH;
  private double HEIGHT;
  @FXML private AnchorPane wallPane;
    private Box<TweetBubbleController>[] boxes;
    private int numberOfRows, numberOfColumns;

  /** This method check if there is an "available" place on the wall */
  private boolean isFull() {
    // Do your implementation
  }

  /** Return the X and Y pos for the next "empty place" */
  private Coupl<Double, Double> nextEmptyBox() {
    // Do your implementation
  }

  /** Throwing the exception here is just to get the code simplier */
  public void addTweet(Tweet tweet) throws IOException {
    if (tweet != null) {
      if (isFull()) {
        // If the wall is full, do some animation of your bubbles
      }
      Couple<Double, Double> emptyPlace = nextEmptyBox();
      // Now load the TweetBubble FXML file
      FXMLLoader loader = new FXMLLoader();
      Parent root = (Parent) loader.load(getClass().getResource("/com/twasyl/tweetwallfx/fxmls/TweetBubble.fxml").openStream());
      
      // Now the file is loaded, place your bubble on the screen
      root.setLayoutX(emptyPlace.getX());
      root.setLayoutY(emptyPlace.getY());
      
      // Get the controller of the bubble and define its content
      TweetBubbleController controller = (TweetBubbleController) loader.getController();
      controller.setContent(tweet);

      // Finally, add the bubble to the screen
      this.wallPane.getChildren().add(root);
    }
  }

  @Override
  public void initialize(URL arg0, ResourceBundle arg1) {
    // Just calculate how much columns and rows you can display
    numberOfColumns = (int) (WIDTH / (TweetBubbleController.DEFAULT_WIDTH + HORIZONTAL_SPACE_BETWEEN_TWEETS));
    numberOfRows = (int) (HEIGHT / (TweetBubbleController.DEFAULT_HEIGHT + VERTICAL_SPACE_BETWEEN_TWEETS));
  }
}

Well this is the main part of the controller. Moving your tweets is algorithmic so go ahead for your own implementation. I’ll do a specific article to present you some animation stuff you’ll be able to use here. The last controller to update is the start screen one. Remember that in the previous article, we defined the onAction=”#startTweetWall” attribute on the search button. Now we’re going to create the method named startTweetWall in our controller which is:

@FXML private void startTweetWall(ActionEvent event) {
  Tab wallTab = null, statsTab = null;
  WallController wallController = null;
  
  // Load the FXML
  try {
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/twasyl/tweetwallfx/fxmls/wall-tab.fxml"));
    wallTab = (Tab) fxmlLoader.load();
    wallController = (WallController) fxmlLoader.getController();
  } catch (IOException ex) {
    Logger.getLogger(TweetWallFXController.class.getName()).log(Level.SEVERE, null, ex);
  }
  if(wallTab != null) {
    // Set the text of the tab by retrieving the text
    // entered in the search text field, binded in
    // our controller
    wallTab.setText(String.format("#%1$s", this.hashtagTextField.getText()));
    
    // Add the tab to the TabPane of our application
    // also binded in our controller        
    this.tabPane.getTabs().add(wallTab);
  }
}

Well this is it. Now what you have to do for example in order to generate tweets is to create a basic TimerTask taking your instance of WallController into parameter, and then, with your DAO, generate tweets, and add them using the addTweet(Tweet t); method of your controller.
Well that’s it for this article. Stay tuned.

TweetWallFX – Screencast 1

Hi,

Here is a short screencast to present TweetWallFX that I’m currently developing. The application is still under development. But it could be really interesting to see some features natively available in JavaFX.