JavaFX font face dialog in Clojure

被刻印的时光 ゝ 提交于 2020-01-17 04:12:47

问题


I'm building a Clojure program that includes an option dialog where the user can select the font used in an editor. Just like many other programs, I would like to present the user with a ComboBox where the dropdown list displays the font names in the font itself (e.g. 'Calibri' is displayed in the Calibri font, 'Arial' is displayed in Arial, and so on.)

In the past in Java, I have used a cell factory to customize the appearance of each cell in the list.

My translation into Clojure is not working though.

Here is what I have come up with so far:

(defn build-font-list-cell
"Return a Cell with an overridden updateItem implementation for 
the cells in the font list combo. Format the name of the font in
the actual font."
[]
(proxy [TextFieldListCell] []
  (updateItem [^String family mt]
    (proxy-super updateItem family mt)
    (if mt
      (.setText this nil)
      (do
        (.setFont this (Font/font family))
        (.setText this family))))))

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

(defn build-font-face-combo
  "Build, configure, and return the combo box used to select the font
  face for the editor."
  []
  (let [family-list (FXCollections/observableArrayList (Font/getFamilies))
    font-face-combo (ComboBox. family-list)
    current-face @tentative-font-face]

    (.setEditable font-face-combo true)
    (.addListener (.selectedItemProperty (.getSelectionModel font-face-combo))
                  ^ChangeListener (face-combo-listener font-face-combo))
    (.setCellFactory font-face-combo (build-font-list-cell-factory))
    (select-item-in-combo font-face-combo current-face)
    font-face-combo))

The compiler throws a ExceptionInInitializerError on this in the build-font-list-cell function at the declaration of the proxy. The IDE (IntelliJ) shows a warning about the updateItem argument in the call to 'super-proxy`, saying it cannot be resolved. I don't understand why not since it doesn't complain about the override on the line above.

This seems like a relatively straightforward translation of Java code that has worked before, but I'm clearly missing something. Or is this even the right approach to take?

EDIT: Adding the following MCVE. It compiles and runs as shown, but does not format the font face names of course. Attempting to create a cell factory by un-commenting the code in the listing produces something that the compiler chokes on.

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

;(defn build-font-list-cell []
;  (proxy [TextFieldListCell] []
;    (updateItem [family mt]
;      (proxy-super updateItem family mt)
;      (if mt
;        (.setText this nil)
;        (do
;          (.setFont this (Font/font family))
;          (.setText this family))))))

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

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

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

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

Another difference from the Java version is that the list cell in Java is a ListCell. But I need to call super.updateItem. As I understand the docs, proxy does not allow you to call super unless the method is public. It is protected in ListCell, but public in TextFieldListCell.

EDIT #2: Here is an example of code that works in Java that I keep referring to.

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);
    }
}

回答1:


There is no this in Clojure. I suspect that is the heart of the error.

The method signature is

public void updateItem(T item, boolean empty)

I would also suggest deleting the unnecessary ^String hint. If you haven't seen it yet, there are some examples on ClojureDocs.org




回答2:


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 I 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.

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 the compiler. 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)

I've put up a Mercurial repo on Bitbucket including an IntelliJ IDEA project for anyone interested.

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.



来源:https://stackoverflow.com/questions/43002816/javafx-font-face-dialog-in-clojure

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!