4
\$\begingroup\$

TextEditorApp

(See this repository for full code.)

Now I have rolled a simple class extending JavaFX Canvas for showing terminal like, colorful console:

TextUIWindow.java:

package com.github.coderodde.ui;

import javafx.scene.canvas.Canvas;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javafx.event.EventHandler;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.FontWeight;

/**
 * This class implements a simple colorful terminal window.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Jul 24, 2022)
 * @since 1.6 (Jul 24, 2022)
 */
public class TextUIWindow extends Canvas {

    private static final int MINIMUM_WIDTH = 1;
    private static final int MINIMUM_HEIGHT = 1;
    private static final int MINIMUM_FONT_SIZE = 1;
    private static final Color DEFAULT_TEXT_BACKGROUND_COLOR = Color.BLACK;
    private static final Color DEFAULT_TEXT_FOREGROUND_COLOR = Color.WHITE;
    private static final Color DEFAULT_BLINK_BACKGROUND_COLOR = Color.WHITE;
    private static final Color DEFAULT_BLINK_FOREGROUND_COLOR = Color.BLACK;
    private static final char DEFAULT_CHAR = ' ';
    private static final String FONT_NAME = "Monospaced";
    private static final int DEFAULT_CHAR_DELIMITER_LENGTH = 4;

    private final int width;
    private final int height;
    private final int fontSize;
    private final Font font;
    private final int fontCharWidth;
    private final int fontCharHeight;
    private final int charDelimiterLength;
    private int windowTitleBorderThickness;
    private final Set<TextUIWindowMouseListener> mouseMotionListeners = 
            new HashSet<>();

    private final Set<TextUIWindowKeyboardListener> keyboardListeners =
            new HashSet<>();

    private final Color[][] backgroundColorGrid;
    private final Color[][] foregroundColorGrid;
    private final boolean[][] cursorGrid;
    private final char[][] charGrid;
    private Color textBackgroundColor = DEFAULT_TEXT_BACKGROUND_COLOR;
    private Color textForegroundColor = DEFAULT_TEXT_FOREGROUND_COLOR;
    private Color blinkCursorBackgroundColor = DEFAULT_BLINK_BACKGROUND_COLOR;
    private Color blinkCursorForegroundColor = DEFAULT_BLINK_FOREGROUND_COLOR;

    public TextUIWindow(int width, int height, int fontSize) {
        this(width, height, fontSize, DEFAULT_CHAR_DELIMITER_LENGTH);
    }

    public TextUIWindow(int width, 
                        int height, 
                        int fontSize, 
                        int charDelimiterLength) {

        this.width = checkWidth(width);
        this.height = checkHeight(height);
        this.fontSize = checkFontSize(fontSize);
        this.charDelimiterLength = 
                checkCharDelimiterLength(charDelimiterLength);

        this.font = getFont();
        this.fontCharWidth = getFontWidth();
        this.fontCharHeight = getFontHeight();

        backgroundColorGrid = new Color[height][width];
        foregroundColorGrid = new Color[height][width];
        charGrid = new char[height][width];
        cursorGrid = new boolean[height][width];

        setDefaultForegroundColors();
        setDefaultBackgroundColors();
        setChars();

        this.setWidth(width * (fontCharWidth + charDelimiterLength));
        this.setHeight(height * fontCharHeight);
        this.setFocusTraversable(true);
        this.addEventFilter(MouseEvent.ANY, (e) -> this.requestFocus());

        setMouseListeners();
        setMouseMotionListeners();
        setKeyboardListeners();
    }

    public Color getTextForegroundColor() {
        return textForegroundColor;
    }

    public Color getTextBackgroundColor() {
        return textBackgroundColor;
    }

    public Color getBlinkCursorForegroundColor() {
        return blinkCursorForegroundColor;
    }

    public Color getBlinkCursorBackgroundColor() {
        return blinkCursorBackgroundColor;
    }

    public void setForegroundColor(Color color) {
        textForegroundColor = 
                Objects.requireNonNull(color, "The input color is null.");
    }

    public void setBackgroundColor(Color color) {
        textBackgroundColor = 
                Objects.requireNonNull(color, "The input color is null.");
    }

    public void turnOffBlink(int charX, int charY) {
        if (checkXandY(charX, charY)) {
            cursorGrid[charY][charX] = false;
        }
    }

    public void setBlinkCursorBackgroundColor(Color backgroundColor) {
        this.blinkCursorBackgroundColor =
                Objects.requireNonNull(
                        backgroundColor, 
                        "backgroundColor is null.");
    }

    public void setBlinkCursorForegroundColor(Color foregroundColor) {
        this.blinkCursorForegroundColor =
                Objects.requireNonNull(
                        foregroundColor, 
                        "foregroundColor is null.");
    }

    public void setTextBackgroundColor(Color backgroundColor) {
        this.textBackgroundColor =
                Objects.requireNonNull(backgroundColor, 
                                       "The input color is null.");
    }

    public void setTextForegroundColor(Color foregroundColor) {
        this.textForegroundColor =
                Objects.requireNonNull(foregroundColor, 
                                       "The input color is null.");
    }

    public int getGridWidth() {
        return width;
    }

    public int getGridHeight() {
        return height;
    }

    public void toggleBlinkCursor(int charX, int charY) {
        if (checkXandY(charX, charY)) {
            cursorGrid[charY][charX] = !cursorGrid[charY][charX];
        }
    }

    public boolean readCursorStatus(int charX, int charY) {
        if (!checkX(charX)) {
            throw charXToException(charX);
        }

        if (!checkY(charY)) {
            throw charYToException(charY);
        }

        return cursorGrid[charY][charX];
    }

    public void printString(int charX, int charY, String text) {
        if (!checkY(charY)) {
            return;
        }

        for (int i = 0; i < text.length(); ++i) {
            setChar(charX + i, charY, text.charAt(i));

            if (!checkX(charX + i)) {
                // Once here, the input text string proceeds beyond the right
                // border. Nothing to print, can exit.
                return;
            }
        }
    }

    public void addTextUIWindowMouseListener(
            TextUIWindowMouseListener listener) {
        mouseMotionListeners.add(listener);
    }

    public void removeTextUIWindowMouseListener(
            TextUIWindowMouseListener listener) {
        mouseMotionListeners.remove(listener);
    }

    public void addTextUIWindowKeyboardListener(
            TextUIWindowKeyboardListener listener) {
        keyboardListeners.add(listener);
    }

    public void removeTextUIWindowKeyboardListener(
            TextUIWindowKeyboardListener listener) {
        keyboardListeners.remove(listener);
    }

    private void setMouseListeners() {
        setMouseClickedListener();
        setMouseEnteredListener();
        setMousePressedListener();
        setMouseReleasedListener();
        setMouseExitedListener();
    }

    private void setMouseMotionListeners() {
        setMouseMovedListener();
        setMouseDraggedListener();
    }

    private void setKeyboardListeners() {
        setKeyboardPressedListener();
        setKeyboardReleaseListener();
        setKeyboardTypedListener();
    }

    private void setKeyboardPressedListener() {
        this.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                for (TextUIWindowKeyboardListener listener 
                        : keyboardListeners) {
                    listener.onKeyPressed(event);
                }
            }
        });
    }

    private void setKeyboardReleaseListener() {
        this.setOnKeyReleased(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                for (TextUIWindowKeyboardListener listener 
                        : keyboardListeners) {
                    listener.onKeyReleased(event);
                }
            }
        });
    }

    private void setKeyboardTypedListener() {
        this.addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
            public void handle(KeyEvent event) {
                for (TextUIWindowKeyboardListener listener : keyboardListeners) {
                    listener.onKeyTyped(event);
                }
            }
        });
    }

    private void setMouseMovedListener() {
        this.setOnMouseMoved(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseMove(event, charX, charY);
                }
            }
        });
    }

    private void setMouseDraggedListener() {
        this.setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseClick(event, charX, charY);
                }
            }
        });
    }

    private void setMouseClickedListener() {
        this.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseClick(event, charX, charY);
                }
            }
        });
    }

    private void setMouseEnteredListener() {
        this.setOnMouseEntered(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseEntered(event, charX, charY);
                }
            }
        });
    }

    private void setMouseExitedListener() {
        this.setOnMouseExited(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseExited(event, charX, charY);
                }
            }
        });
    }

    private void setMousePressedListener() {
        this.setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMousePressed(event, charX, charY);
                }
            }
        });
    }

    private void setMouseReleasedListener() {
        this.setOnMouseReleased(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                int pixelX = (int) event.getX();
                int pixelY = (int) event.getY();

                int charX = convertPixelXtoCharX(pixelX);
                int charY = convertPixelYtoCharY(pixelY);

                for (TextUIWindowMouseListener listener 
                        : mouseMotionListeners) {
                    listener.onMouseReleased(event, charX, charY);
                }
            }
        });
    }

    private int convertPixelXtoCharX(int pixelX) {
        return pixelX / (fontCharWidth + charDelimiterLength);
    }

    private int convertPixelYtoCharY(int pixelY) {
        return (pixelY - windowTitleBorderThickness) / fontCharHeight;
    }

    public void setTitleBorderThickness(int thickness) {
        this.windowTitleBorderThickness = thickness;
    }

    public void repaint() {
        GraphicsContext gc = getGraphicsContext2D();

        for (int y = 0; y < height; y++) {
            repaintRow(gc, y);
        }
    }

    private void repaintRow(GraphicsContext gc, int y) {
        for (int x = 0; x < width; x++) {
            repaintCell(gc, x, y);
        }
    }

    private void repaintCell(GraphicsContext gc, int x, int y) {
        repaintCellBackground(gc, x, y);
        repaintCellForeground(gc, x, y);
    }

    private void repaintCellBackground(GraphicsContext gc,  
                                       int charX, 
                                       int charY) {
        if (cursorGrid[charY][charX]) {
            // Once here, we need to use the cursor's color:
            gc.setFill(blinkCursorBackgroundColor);
        } else {
            gc.setFill(backgroundColorGrid[charY][charX]);
        }

        gc.fillRect(charX * (fontCharWidth + charDelimiterLength),
                    charY * fontCharHeight,
                    fontCharWidth + charDelimiterLength,
                    fontCharHeight);
    }

    private void repaintCellForeground(GraphicsContext gc,
                                       int charX, 
                                       int charY) {
        gc.setFont(font);

        if (cursorGrid[charY][charX]) {
            gc.setFill(blinkCursorForegroundColor);
        } else {
            gc.setFill(foregroundColorGrid[charY][charX]);
        }

        int fixY = fontCharHeight - (int) getFontMetrics().getMaxAscent();

        gc.fillText("" + charGrid[charY][charX],
                    charDelimiterLength / 2 +
                            (fontCharWidth + charDelimiterLength) * charX,
                    fontCharHeight * (charY + 1) - fixY);
    }

    public Color getForegroundColor(int charX, int charY) {
        if (!checkX(charX)) {
            throw charXToException(charX);
        }

        if (!checkY(charY)) {
            throw charYToException(charY);
        }

        return foregroundColorGrid[charY][charX];
    }

    public Color getBackgroundColor(int charX, int charY) {
        if (!checkX(charX)) {
            throw charXToException(charX);
        }

        if (!checkY(charY)) {
            throw charYToException(charY);
        }

        return backgroundColorGrid[charY][charX];
    }

    public void setForegroundColor(int charX, int charY, Color color) {
        if (checkXandY(charX, charY)) {
            foregroundColorGrid[charY][charX] = 
                    Objects.requireNonNull(color, "The color is null.");
        }
    }

    public void setBackgroundColor(int x, int y, Color color) {
        if (checkXandY(x, y)) {
            backgroundColorGrid[y][x] = 
                    Objects.requireNonNull(color, "The color is null.");
        }
    }

    public char getChar(int charX, int charY) {
        if (!checkX(charX)) {
            throw charXToException(charX);
        }

        if (!checkY(charY)) {
            throw charYToException(charY);
        }

        return charGrid[charY][charX];
    }

    public void setChar(int x, int y, char ch) {
        if (checkXandY(x, y)) {
            charGrid[y][x] = ch;
            foregroundColorGrid[y][x] = textForegroundColor;
            backgroundColorGrid[y][x] = textBackgroundColor;
        }
    }

    public int getPreferredWidth() {
        return width * (fontCharWidth + charDelimiterLength);
    }

    public int getPreferredHeight() {
        return height * fontCharHeight;
    }

    private FontMetrics getFontMetrics() {
        return Toolkit.getToolkit().getFontLoader().getFontMetrics(font);
    }

    private static int checkWidth(int widthCandidate) {
        if (widthCandidate < MINIMUM_WIDTH) {
            throw new IllegalArgumentException(
                    "Width candidate is invalid (" 
                            + widthCandidate
                            + "). Must be at least " 
                            + MINIMUM_WIDTH
                            + ".");
        }

        return widthCandidate;
    }

    private static int checkHeight(int heightCandidate) {
        if (heightCandidate < MINIMUM_WIDTH) {
            throw new IllegalArgumentException(
                    "Height candidate is invalid (" 
                            + heightCandidate
                            + "). Must be at least " 
                            + MINIMUM_HEIGHT
                            + ".");
        }

        return heightCandidate;
    }

    private static int checkFontSize(int fontSizeCandidate) {
        if (fontSizeCandidate < MINIMUM_FONT_SIZE) {
            throw new IllegalArgumentException(
                    "Font size candidate is invalid (" 
                            + fontSizeCandidate
                            + "). Must be at least " 
                            + MINIMUM_FONT_SIZE
                            + ".");
        }

        return fontSizeCandidate;
    }

    private int checkCharDelimiterLength(int charDelimiterLength) {
        if (charDelimiterLength < 0) {
            throw new IllegalArgumentException(
                    "Char delimiter length negative: (" 
                            + charDelimiterLength 
                            + "). Must be at least 0.");
        }

        return charDelimiterLength;
    }

    private IndexOutOfBoundsException charXToException(int charX) {
        if (charX < 0) {
            return new IndexOutOfBoundsException(
                    "Character X coordinate is negative: " + charX);
        }

        if (charX >= width) {
            return new IndexOutOfBoundsException(
                    "Character X coordinate is too large: " 
                            + charX
                            + ". Must be at most "
                            + (width - 1)
                            + ".");
        }

        throw new IllegalStateException("Should not get here.");
    }

    private IndexOutOfBoundsException charYToException(int charY) {
        if (charY < 0) {
            throw new IndexOutOfBoundsException(
                    "Character Y coordinate is negative: " + charY);
        }

        if (charY >= height) {
            throw new IndexOutOfBoundsException(
                    "Character Y coordinate is too large: " 
                            + charY
                            + ". Must be at most "
                            + (height - 1)
                            + ".");
        }

        throw new IllegalStateException("Should not get here.");
    }

    private boolean checkX(int x) {
        return x >= 0 && x < width;
    }

    private boolean checkY(int y) {
        return y >= 0 && y < height;
    }

    private boolean checkXandY(int x, int y) {
        return checkX(x) && checkY(y);
    }

    private void setDefaultForegroundColors() {
        for (Color[] colors : foregroundColorGrid) {
            for (int i = 0; i < width; i++) {
                colors[i] = DEFAULT_TEXT_FOREGROUND_COLOR;
            }
        }
    }

    private void setDefaultBackgroundColors() {
        for (Color[] colors : backgroundColorGrid) {
            for (int i = 0; i < width; i++) {
                colors[i] = DEFAULT_TEXT_BACKGROUND_COLOR;
            }
        }
    }

    private void setChars() {
        for (char[] charRow : charGrid) {
            for (int i = 0; i < width; i++) {
                charRow[i] = DEFAULT_CHAR;
            }
        }
    }

    private Font getFont() {
        return Font.font(FONT_NAME, FontWeight.BOLD, fontSize);
    }

    private int getFontWidth() {
        return (int) getFontMetrics().getCharWidth('C') + charDelimiterLength;
    }

    private int getFontHeight() {
        return (int) getFontMetrics().getLineHeight();
    }
}

TextEditorApp.java:

package com.github.coderodde.ui;

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.ColorPicker;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * This class implements a simple demo text editor.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Jul 14, 2022)
 * @since 1.6 (Jul 14, 2022)
 */
public class TextEditorApp extends Application {

    private static final int CHAR_GRID_WIDTH = 40;
    private static final int CHAR_GRID_HEIGHT = 24;
    private static final int FONT_SIZE = 17;
    private static final int CHAR_HORIZONTAL_DELIMITER_LENGTH = 1;
    private static final int SLEEP_MILLISECONDS = 400;
    private static final String HELLO_WORLD_STRING = "Hello, world! ";

    private final TextUIWindow window;
    private final HelloWorldThread helloWorldThread;
    private final CursorBlinkThread cursorBlinkThread;

    private volatile int cursorX = 0;
    private volatile int cursorY = 2;

    public TextEditorApp() {
        this.window = new TextUIWindow(CHAR_GRID_WIDTH,
                                       CHAR_GRID_HEIGHT,
                                       FONT_SIZE,
                                       CHAR_HORIZONTAL_DELIMITER_LENGTH);

        this.helloWorldThread = new HelloWorldThread();
        this.cursorBlinkThread = new CursorBlinkThread();
    }

    @Override
    public void stop() {
        helloWorldThread.requestExit();
        cursorBlinkThread.requestExit();

        try {
            helloWorldThread.join();
        } catch (InterruptedException ex) {

        }

        try {
            cursorBlinkThread.join();
        } catch (InterruptedException ex) {

        }
    }

    @Override
    public void start(Stage primaryStage) {
        Platform.runLater(() -> {

            try {
                ColorPicker textForegroundColorPicker = 
                        new ColorPicker(window.getTextForegroundColor());

                ColorPicker textBackgroundColorPicker = 
                        new ColorPicker(window.getTextBackgroundColor());

                ColorPicker cursorBlinkForegroundColorPicker = 
                        new ColorPicker(window.getBlinkCursorForegroundColor());

                ColorPicker cursorBlinkBackgroundColorPicker =
                        new ColorPicker(window.getBlinkCursorBackgroundColor());

                textForegroundColorPicker.setOnAction(new EventHandler() {
                    @Override
                    public void handle(Event t) {
                        Color color = textForegroundColorPicker.getValue();
                        window.setTextForegroundColor(color);
                        window.setForegroundColor(cursorX, cursorY, color);
                    }
                });

                textBackgroundColorPicker.setOnAction(new EventHandler() {
                    @Override
                    public void handle(Event t) {
                        Color color = textBackgroundColorPicker.getValue();
                        window.setTextBackgroundColor(color);
                        window.setBackgroundColor(cursorX, cursorY, color);
                    }
                });

                cursorBlinkForegroundColorPicker.setOnAction(new EventHandler() {
                    @Override
                    public void handle(Event t) {
                        window.setBlinkCursorForegroundColor(
                                cursorBlinkForegroundColorPicker.getValue());
                    }
                });

                cursorBlinkBackgroundColorPicker.setOnAction(new EventHandler() {
                    @Override
                    public void handle(Event t) {
                        window.setBlinkCursorBackgroundColor(
                                cursorBlinkBackgroundColorPicker.getValue());
                    }
                });

                HBox hboxColorPickers = new HBox(textForegroundColorPicker,
                                                 textBackgroundColorPicker,
                                                 cursorBlinkForegroundColorPicker,
                                                 cursorBlinkBackgroundColorPicker);

                VBox vbox = new VBox(hboxColorPickers, window);

                Scene scene = new Scene(vbox, 
                                        window.getPreferredWidth(), 
                                        window.getPreferredHeight() + 35, // How to get rid of this 35?
                                        false,
                                        SceneAntialiasing.BALANCED);

                window.setTitleBorderThickness((int) scene.getY());

                primaryStage.setScene(scene);
                window.addTextUIWindowMouseListener(new TextEditorMouseListener());
                window.addTextUIWindowKeyboardListener(
                        new TextEditorKeyboardListener());

                helloWorldThread.start();
                cursorBlinkThread.start();

                primaryStage.setResizable(false);
                primaryStage.show();
                window.requestFocus();
                window.repaint();
            } catch (Exception ex) {
                ex.printStackTrace();
                helloWorldThread.requestExit();
                cursorBlinkThread.requestExit();

                try {
                    helloWorldThread.join();
                } catch (InterruptedException ex2) {

                }

                try {
                    cursorBlinkThread.join();
                } catch (InterruptedException ex2) {

                }
            }
        });
    }

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

    private void moveCursorUp() {
        if (cursorY == 2) {
            return;
        }

        window.turnOffBlink(cursorX, cursorY);
        cursorY--;
        Platform.runLater(() -> { window.repaint(); });
    }

    private void moveCursorLeft() {
        if (cursorX == 0) {
            if (cursorY > 2) {
                window.turnOffBlink(cursorX, cursorY);
                cursorY--;
                cursorX = window.getGridWidth() - 1;
                Platform.runLater(() -> { window.repaint(); });
            }
        } else {
            window.turnOffBlink(cursorX, cursorY);
            cursorX--;
            Platform.runLater(() -> { window.repaint(); });
        }
    }

    private void moveCursorRight() {
        if (cursorX == window.getGridWidth() - 1) {
            if (cursorY < window.getGridHeight() - 1) {
                window.turnOffBlink(cursorX, cursorY);
                cursorY++;
                cursorX = 0;
                Platform.runLater(() -> { window.repaint(); });
            }
        } else {
            window.turnOffBlink(cursorX, cursorY);
            cursorX++;
            Platform.runLater(() -> { window.repaint(); });
        }
    }

    private void moveCursorDown() {
        if (cursorY == window.getGridHeight() - 1) {
            return;
        }

        window.turnOffBlink(cursorX, cursorY);
        cursorY++;
        Platform.runLater(() -> { window.repaint(); });
    }

    private final class CursorBlinkThread extends Thread {

        private static final long CURSOR_BLINK_SLEEP = 600L;

        private volatile boolean doRun = true;

        @Override
        public void run() {
            while (doRun) {
                try {
                    for (int i = 0; i < 10; i++) {
                        Thread.sleep(CURSOR_BLINK_SLEEP / 10);

                        if (!doRun) {
                            return;
                        }
                    }
                } catch (InterruptedException ex) {

                }

                window.toggleBlinkCursor(cursorX, cursorY);

                Platform.runLater(() -> { window.repaint(); });
            }
        }

        void requestExit() {
            doRun = false;
        }
    }

    private final class HelloWorldThread extends Thread {
        private final char[] textChars = new char[HELLO_WORLD_STRING.length()];
        private volatile boolean doRun = true;

        void requestExit() {
            doRun = false;
        }

        @Override
        public void run() {
            int xOffset = 
                    (window.getGridWidth() - HELLO_WORLD_STRING.length()) / 2;

            List<Character> characterList = 
                    new ArrayList<>(HELLO_WORLD_STRING.length());

            for (char ch : HELLO_WORLD_STRING.toCharArray()) {
                characterList.add(ch);
            }

            while (doRun) {
                try {
                    for (int i = 0; i < 10; i++) {
                        Thread.sleep(SLEEP_MILLISECONDS / 10);

                        if (!doRun) {
                            return;
                        }
                    }
                } catch (InterruptedException ex) {
                    return;
                }

                String text = toString(characterList, textChars);
                window.printString(xOffset, 0, text);
                Character ch = characterList.remove(0);
                characterList.add(ch);

                Platform.runLater(() -> { window.repaint(); });
            }
        }

        private static String toString(List<Character> charList,
                                       char[] chars) {
            for (int i = 0; i < chars.length; i++) {
                chars[i] = charList.get(i);
            }

            return new String(chars);
        }
    }

    private final class TextEditorKeyboardListener 
            implements TextUIWindowKeyboardListener {

        @Override
        public void onKeyTyped(KeyEvent event) {
            window.turnOffBlink(cursorX, cursorY);
            window.setChar(cursorX, cursorY, event.getCharacter().charAt(0));
            moveCursorRight();

            Platform.runLater(() -> { window.repaint(); });
            event.consume();
        }

        @Override
        public void onKeyPressed(KeyEvent event) {
            switch (event.getCode()) {
                case UP:
                    moveCursorUp();
                    break;

                case LEFT:
                    moveCursorLeft();
                    break;

                case RIGHT:
                    moveCursorRight();
                    break;

                case DOWN:
                    moveCursorDown();
                    break;
            }

            event.consume();
        }
    }

    private final class TextEditorMouseListener 
            implements TextUIWindowMouseListener {

        public void onMouseClick(MouseEvent event, int charX, int charY) {
            window.turnOffBlink(cursorX, cursorY);
            cursorX = charX;
            cursorY = charY;
        }
    }
}

Critique request

Can you spot any graphics/listener artifacts? Also, in the app class, on line 130, I have hardcoded the height for the HBox of the color pickers; would like to hear a word or two how to make this in a non-hard coded fashion.

(Some) Running instructions

I use JavaFX SDK 18.0.1 and the VM options:

--module-path "C:\Software\javafx-sdk-18.0.1\lib" --add-modules javafx.base,javafx.controls,javafx.fxml,javafx.graphics --add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED

(Change C:\Software\javafx-sdk-18.0.1\lib to point to your JavaFX SDK lib folder.) Also, I run everything from NetBeans 14. If you want to make a runnable .jar-file, you will need to configure the project pom.xml.

\$\endgroup\$
3
  • \$\begingroup\$ sadly you don't use the Stream API - that would clean up much of your code \$\endgroup\$ Commented Aug 5, 2022 at 12:14
  • \$\begingroup\$ that's fine for me :-) i know it's a sensitive topic ^^ don't worry :-) \$\endgroup\$ Commented Aug 8, 2022 at 12:49
  • \$\begingroup\$ There appear to be a lot of calls to Platform.runLater outside of a Thread. This suggests that you either have bad code design or don't know when to use Platform.runLater. Have a look at this. Hopefully, it will help you identify when to use Platform.runLater. If you run into issues where you use Platform.runLater where you shouldn't, try to find solutions other than using Platform.runLater. \$\endgroup\$ Commented Aug 31, 2022 at 16:14

1 Answer 1

3
\$\begingroup\$

Re-usability of Widget

First, your name is a bit misguiding, since TextUIWindow is not a real Window/Stage (see javafx.stage.Stage) but a mere Canvas.

Second thing is that your image (your Screenshots) provide the feeling, that the TextUIWindow is rather a Widget. And as such one it has a strong coupling to the TextEditorApp.CursorBlinkThread and therefore to the TextEditorApp. Without them, the Widget can`t work properly.

Maybe you could refactor the widget to be more standalone and less dependent?

What if i want to include your TextUIWindow into my App? Do i have to care about the rendering myself? I am not sure if i would do, in this case.

Bugs

bug: incorrect calculation of height (as requested in your post)

(i havent tried out, still updating my IDE with Javafx ^^)

getPreferredHeight(){
  return height * fontCharHeight;
}

looks good so far, but when we inspect on how fontCharHeight is calculated we see:

this.fontCharHeight = getFontHeight();

digging even deeper revelas the source of problems:

private int getFontHeight() {
    return (int) getFontMetrics().getLineHeight(); //LOSS OF PRECISION!!!!
}

here happenes the problem, the line height is rounded... und the round error is multiplied for each row...

minor bug TextUIWindow: line 289, setOnMouseDraggedListener()

for (TextUIWindowMouseListener listener: mouseMotionListeners) { 
    listener.onMouseClick(event, charX, charY); //Should be 'onMouseDragged'
}

minor bug: pom.xml

  • <exec.mainClass>com.github.coderodde.ui.CUIWindow</exec.mainClass> //would be TextEditorApp

minor smells

duplicated code: all mouse Events Handlings have the code

int pixelX = (int) event.getX();
int pixelY = (int) event.getY();
                
int charX = convertPixelXtoCharX(pixelX);
int charY = convertPixelYtoCharY(pixelY);

maybe a simple method could reduce this bloater:

listener.onMouseClick(event, getCharX(event), getCharY(event));

(if you do not like this because it is calculated for each listener then create a proper data object - but how much listener are you expecting ^^)

missing test:

your repository does not contain any tests...

\$\endgroup\$
1
  • \$\begingroup\$ honestly my focus was only on TextUIWindow primary... \$\endgroup\$ Commented Aug 5, 2022 at 10:47

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.