JavaFX: Fixed size label

Though JavaFX provides a component for Pagination this is based on a fixed number of pages. However consider the following use case:

You have some text of undefined length but definitely longer than can fit on a page. The text is dynamic so it is not feasible to split up the text onto several pages. Further more it may be possible that while displaying the font or the font style changes and thereby resulting in a different paging of the text.

As I have just such a use case in OpenPatrician, I did a quick test on how such a thing could be implemented.

The implemented solution is a simplification of the above use case as I only tested for a single line. However from here it is possible to add multiple lines and when the page is full, add another page with a next button.

In the example I continually add more text but not more will be displayed but I get the not displayed text back.

Without further here is the Control component:

package javafxtest.label;

import java.util.StringTokenizer;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Control;
import javafx.scene.text.Font;

/**
 * Text control that has a fixed size text field. Pushing more
 * text than is displayable into it will result in an exception.
 * @author andi
 */
public class FixedSizedText extends Control {
    private final DoubleProperty localWidth;
    private final DoubleProperty localHeigth;
    private final StringProperty text;
    private final ReadOnlyObjectWrapper<Font> font;

    public FixedSizedText() {
        localWidth = new SimpleDoubleProperty(this, "width");
        localHeigth = new SimpleDoubleProperty(this, "heigth");
        text = new SimpleStringProperty(this, "text", "");
        font = new ReadOnlyObjectWrapper<>(this, "font");
        getStyleClass().add("fixed-size-text");
    }

    @Override
    protected String getUserAgentStylesheet() {
        return getClass().getResource("/javafxtest/label/"+getClass().getSimpleName()+".css").toExternalForm();
    }
    /**
     * Fill in as much of the text as possible and return the rest.
     * @param text to be filled in
     * @return not filled in text
     */
    public String fillIn(String text) {
        final StringTokenizer tokenizer = new StringTokenizer(text, " ");
        //final String oldText = getText();
        try {
            while (tokenizer.hasMoreTokens()) {
                String next = tokenizer.nextToken();
                setText(getText()+" "+next);
            }
        } catch (FixedSizeTextOverflowException e) {
            StringBuilder sb = new StringBuilder();
            while (tokenizer.hasMoreTokens()) {
                sb.append(" ").append(tokenizer.nextToken());
            }
            return sb.toString();
        }
        return "";
    }  

    public double getLocalHeigth() {
        return localHeigth.get();
    }

    public void setLocalHeigth(double value) {
        localHeigth.set(value);
    }

    public DoubleProperty localHeigthProperty() {
        return localHeigth;
    }

    public double getLocalWidth() {
        return localWidth.get();
    }

    public void setLocalWidth(double value) {
        localWidth.set(value);
    }

    public DoubleProperty localWidthProperty() {
        return localWidth;
    }

    public String getText() {
        return text.get();
    }

    public void setText(String value) {
        text.set(value);
    }

    public StringProperty textProperty() {
        return text;
    }
    public Font getFont() {
        return font.get();
    }

    public ReadOnlyObjectWrapper<Font> fontProperty() {
        return font;
    }

}

The most important method is the fillIn method which will gruadualy append word by word to the components text property. There is this funny bit about catching a FixedSizeTextOverflowException which is a RuntimeException. To understand this properly we have to take a look at the skin:

public class FixedSizedTextSkin extends SkinBase<FixedSizedText> {

    private final Label label;

    public FixedSizedTextSkin(FixedSizedText control) {
        super(control);
        label = new Label();
        label.setWrapText(false);
        StyleChangeListener styleListener = new StyleChangeListener();
        control.styleProperty().addListener(styleListener);
        //FontChangeListener fontChangeListener = new FontChangeListener();
        //control.fontProperty().addListener(fontChangeListener);
        control.textProperty().addListener(new TextChangeListener());
        getChildren().add(label);
    }
    /**
     * The text style may be defined style, therefore it must be checked when changeing.
     */
    private class StyleChangeListener implements ChangeListener<String> {

         @Override
        public void changed(ObservableValue<? extends String> ov, String t, String t1) {
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }

    }
    /**
     * If the text changes make sure all can be displayed.
     */
    private class TextChangeListener implements ChangeListener<String> {

         @Override
        public void changed(ObservableValue<? extends String> observable, String oldText, String newText) {
            final double maxWidth = getSkinnable().localWidthProperty().getValue();
            final double maxHeigth = getSkinnable().localHeigthProperty().getValue();
            if (label.getWidth()>maxWidth || label.getHeight()>maxHeigth) {
                throw new FixedSizeTextOverflowException();
            }
            label.setText(newText);
            if (label.getWidth()>maxWidth || label.getHeight()>maxHeigth) {
                label.setText(oldText);
                throw new FixedSizeTextOverflowException();
            }
        }

    }
    /**
     * If the font changes this may have an impact on what can be displayed.
     */
        private class FontChangeListener implements ChangeListener<Font> {

         @Override
        public void changed(ObservableValue<? extends Font> ov, Font t, Font t1) {
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }    
    }
}

On the text property of the control a listener is registered, which will check that the size of the label with the new text does not exceed the width and height constraint of the control. If they are exceeded a FixedSizeTextOverflowException will be thrown. Besides changes on the text itself we should also listen to font changes and style changes, as well as size changes of the control.

The test code looks like this:

package javafxtest.label;

import com.sun.javafx.tk.TKPulseListener;
import com.sun.javafx.tk.Toolkit;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class FixedSizedTextTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        FixedSizedText text = new FixedSizedText();
        text.setLocalHeigth(20);
        text.setLocalWidth(200);

        StackPane root = new StackPane();
        root.getChildren().add(text);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("Fixed size table");
        primaryStage.setScene(scene);
        primaryStage.show();
        Toolkit.getToolkit().addSceneTkPulseListener(new LabelGrowth(text));
    }

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

        private static class LabelGrowth implements TKPulseListener {

        private final FixedSizedText text;
        private int counter = 0;

        public LabelGrowth(FixedSizedText label) {
            this.text = label;
        }

        @Override
        public void pulse() {
                String exeeding = text.fillIn(text.getText()+"blah ");
                System.out.println("Update no. "+counter+" to: "+text.getText());
                System.out.println("Exeeding text: "+exeeding);
                counter++;
        }        
    }
}

Schreibe einen Kommentar