Overview

HTTPS SSH

ffcbd

(Font Face ComboBox Demo)

A Clojure application designed to demonstrate how to create a styled JavaFX combo box for font selection in Clojure.

The Problem

Many applications provide a way for a user to select a particular font to use for some purpose. A common example is a word processor that allows some portion of the text to be formatted with a particular font. This is often accomplished by presenting a combo box with a drop down list showing the font names rendered in the font itself. For example, here is a section of such a combo box presented by LibreOffice Writer on my desktop.

A similar control can be created in JavaFX using a ComboBox with a customized Cell factory that can format each item in the list of fonts.

Here is an example of a small Java program that constructs and displays such a control.

    package FontFaceDialog;

    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.text.Font;
    import javafx.stage.Stage;
    import javafx.scene.control.ComboBox;
    import javafx.util.Callback;

    public class Main extends Application {

        private ComboBox<String> buildFontFaceCombo() {
            ObservableList<String> lst = FXCollections.observableList(javafx.scene.text.Font.getFamilies());
            ComboBox<String> cb = new ComboBox<String>(lst);
            cb.getSelectionModel().select(0);
            cb.setCellFactory((new Callback<ListView<String>, ListCell<String>>() {
                @Override
                public ListCell<String> call(ListView<String>     listview) {
                    return new ListCell<String>() {
                        @Override
                        protected void updateItem(String family,  boolean empty) {
                            super.updateItem(family, empty);
                            if (empty) {
                                setText(null);
                            } else {
                                setFont(Font.font(family));
                                setText(family);
                            }
                        }
                    };
                }
            }));
            return cb;
        }

        @Override
        public void start(Stage primaryStage) throws Exception {
            BorderPane root = new BorderPane();
            root.setTop(buildFontFaceCombo());
            primaryStage.setTitle("Font Face Dialog Example");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }

        public static void main(String[] args) {
            launch(args);
        }
    }

All well and good, but the program I'm writing is in Clojure. I want the same functionality in my program. But it isn't clear how the Cell factory translates into Clojure. The Cell factory essentially returns an object with an updateItem method that overrides that of the base class, in this case a ListView. (There is a similar set of classes for tables.)

One Approach

Unfortunately, I have been unable to figure out how to do this in Clojure so far. Since I need to get this working, I'm using another alternative. Since we know how to do it in Java, why not just do the mystery bits in Java. It's not as aesthetically pleasing, but it works.

Working with polyglot programs in lein is possible but kind of fiddly.

First, here is the Clojure part of the demo.

    ns ffcbd.core
      (:gen-class
        :extends javafx.application.Application)
      (:import (com.example FontFaceListCell)
               (javafx.application Application)
               (javafx.collections FXCollections)
               (javafx.scene.control ComboBox)
               (javafx.scene.text Font)
               (javafx.scene.layout BorderPane)
               (javafx.scene Scene)
               (javafx.stage Stage)
               (javafx.util Callback)))

    (defn build-font-list-cell-factory []
      (proxy [Callback] []
        (call [list-view]
          (FontFaceListCell.))))

    (defn build-font-face-combo []
      (let [family-list (FXCollections/observableArrayList (Font/getFamilies))
            font-face-combo (ComboBox. family-list)]
        (.setCellFactory font-face-combo (build-font-list-cell-factory))
        (.select (.getSelectionModel font-face-combo) 0)
        font-face-combo))

    (defn -start [this stage]
      (let [root (BorderPane.)
            scene (Scene. root)]

        (.setTop root (build-font-face-combo))
        (.add (.getChildren root) (build-font-face-combo))
        (.setMinSize root 400 275)

        (doto stage
          (.setScene scene)
          (.setTitle "Font Face ComboBox Demo")
          (.show))))

    (defn -main [& args]
      (Application/launch ffcbd.core args))

Notice the import of com.example.FontFaceListCell near the top. That's the Java part. Here's the listing of that little class.

    package com.example;

    import javafx.scene.control.ListCell;
    import javafx.scene.text.Font;

    public class FontFaceListCell extends ListCell<String> {
        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
            } else {
                setFont(Font.font(item, 16.0d));
                setText(item);
            }
        }
    }

This class extends ListCell and overrides the updateItem method. When you run the program and click on the ComboBox, I get something like this on my system.

Pretty neat huh?

As mentioned above, to get this working with lein, you need a few more fiddly bits.

In order to compile Java code, lein needs to know where Java is. I added this line to my global profile.

    :java-cmd "C:\\Program Files\\Java\\jdk1.8.0_121\\bin\\java.exe"

This kinda sucks because now I can't use the same profiles.clj file on Windows and Linux.

Getting this to work also requires a few changes to the project.clj to tell lein where the Java code is and which options to pass to it. Here is what I used.

    (defproject ffcbd "0.1.0-SNAPSHOT"
      :description "A demo of a styled font selection ComboBox in Clojure."
      :dependencies [[org.clojure/clojure "1.8.0"]]
      :java-source-paths ["java"]
      :javac-options     ["-target" "1.8" "-source" "1.8"]
      :aot :all
      :main ffcbd.core)

It would still be nice to get all of this working only using Clojure though. I'm sure gen-class would do it, but haven't figured it out yet.