From parameter files, to raw data, to debuggin, it can be really useful to manually load text files natively in the application you’re trying to develop. That way you can ensure that the information your program is ingesting is the stuff you meant to put in.

It’s also the first step on the way to creating more complex applications like hex editors and development aids. in this article, I’ll go through the step by step process of building that editor.

A Simple JavaFX Text Editor

In JavaFX, text can be loaded and displayed in a TextArea by parsing text using a BufferedReader as a list of strings. This can be trasnferred to a TextArea by concatenating the list and invoking setText(String) on the TextArea.

What you’ll get from this article

The user interface in a text reader needs to have three main components:

  1. A route through which the user can select a file. This can be drag-and-drop, or through a file chooser, but I’ll just use a file chooser for today.
  2. A Text area to display the contents of the file.

Additionally, we can upgrade this functionality in two ways:

  1. Running a background process to determine if the file has been edited outside of the application
  2. Enabling the user to save the file themselves through either keyboard shortcuts, or menu selection.
  3. Keeping track of the current file status so that if a user loads in another text file, they are prompted should changes need to be saved.

By the end of it, we will have a basic text editor capable of loading, synching and saving a text document.

JavaFX Application information flow and method calls for a basic text editor

Oh my days, I just need the code please :)!

That’s OK. There’s a full code example at the end of a working JavaFX Text Editor. You can jump down with this button and read the rest when you have the time.

Jump To Code!

Designing the User Interface

To facilitate an interface that can open, edit, sync and save a text file, we’ll need three basic elements to our view.

A TextA

  1. A simple menu to open, save and close the file
  2. A TextArea to display the contents of the file
  3. A status label and progress bar to display loading progress
  4. A button to let the user load changes if they occur outside of the program
Sime File Editor FXML WireFrame

I’ve tried to keep it as simple as possible – really just the bare bones of what you need. You may want to improve and style it yourself if you use the program, but here’s the simple FXML markup to get you started:

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.edencoding.controllers.SimpleFileEditorController">
   <top>
       <MenuBar>
           <Menu text="File">
               <MenuItem text="Open" />
               <MenuItem text="Save" />
               <MenuItem text="Close" />
           </Menu>
       </MenuBar>
   </top>
   <center>
       <AnchorPane>
           <TextArea editable="false" prefHeight="400.0" prefWidth="600.0" text="Load a text file using the menu" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0" />
       </AnchorPane>
   </center>
   <bottom>
       <HBox>
           <padding>
               <Insets bottom="5.0" left="5.0" right="5.0" />
           </padding>
           <HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS">
               <Label fx:id="statusMessage" prefWidth="150.0" text="Checking for Changes..." />
               <ProgressBar fx:id="progressBar" prefWidth="150.0" progress="0.0" />
           </HBox>
           <HBox alignment="CENTER_RIGHT" HBox.hgrow="ALWAYS">
               <Button fx:id="loadChangesButton" mnemonicParsing="false" text="Load Changes" />
           </HBox>
       </HBox>
   </bottom>
</BorderPane>

Note: I’ve exposed the Label, ProgressBar and Button using fx:id tags, because I’ll want to control when they’re visible based on the application’s current state.

Finally, we’ll create the Controller, which will define the actual business logic of loading a file into our View, and maintaining it in the programme’s memory. You can see I’ve already linked the FXML file to a Java file using the attribute fx:controller. You’ll need to link yours up too.

A basic Controller doesn’t actually need any code in it. it should by default look like this:

package com.edencoding.controllers;

public class SimpleFileEditorController {
    public void initialize(){
        loadChangesButton.setVisible(false);
    }
}

What I have added is a little code to hide the “load file changes” button, because we won’t want to see that until we’ve detected changes later on. There are actually a lot of ways to hide a button in JavaFX, with different effects on the interface. Here’s setting the visible property will suit just fine.

Next, we’ll add the Java code to control how users open a text file.

How to display a text file in a TextArea

We’ll use JavaFX events to start the sequence of loading a file into the text area in four stages:

  1. Set an action on the “Open” menu item using the FXML # operator
  2. Use that method to let the user choose a file to load.
  3. Read a text file into memory
  4. Display the file contents
The loading process for a text editor built with JavaFX

1. Set an action on the “Open” menu item using the FXML # operator

To set a method on a button action, we need to use the onAction attribute of the MenuItem object in FXML.

Inside the FXML file above, modify the “Open” MenuItem so that it looks like this:

<MenuItem text="Open" onAction="#chooseFile"/>

After that, we’ll need to define the chooseFile() method in our Controller.

2. Choosing a file to load

The controller should be looking pretty blank at the moment, so we’ll need to add a method to let our user choose a file when prompted. The prototype for this method is as so:

public void chooseFile(ActionEvent event);

Because it originates from a MenuItem wired in from the FXML file, the method by default takes an ActionEvent as a parameter, which will be automatically generated and passed to the method by JavaFX when the MenuItem is clicked.

Don’t worry, we don’t have to do anything with the event except consume it once we’re finished. If you want to find out more about event propagation, and everything about JavaFX events, check out my comprehensive guide. It goes into depth about how to create them and use them effectively.

Right now we need to let our user load the file. Inside the method:

public void chooseFile(ActionEvent event) {
    FileChooser fileChooser = new FileChooser();

    //only allow text files to be selected using chooser
    fileChooser.getExtensionFilters().add(
            new FileChooser.ExtensionFilter("Text files (*.txt)", "*.txt")
    );

    //set initial directory somewhere user will recognise
    fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));

    //let user select file
    File fileToLoad = fileChooser.showOpenDialog(null);

    //if file has been chosen, load it using asynchronous method (define later)
    if(fileToLoad != null){
        loadFileToTextArea(fileToLoad);
    }
}

What we get at the end of the process is a File object that refers to the data on the hard disk – we haven’t loaded anything into the Application memory yet.

Note: because we’re compartmentalising our code nicely, once we’ve successfully selected a file, we can delegate the next action to another method – loadFileToTextArea(File file), which will do the heavy lifting of loading the file itself.

3. Reading a text file in Java

To read and display the text file, we’ll create a Task that’s going to:

  1. Load our file in the background
  2. Display the contents of the file in the TextArea when it’s done.

Then:

  1. We’ll bind the Task to the ProgressBar, so it updates the user on the file load process. In reality, most small file loads should be fast enough that this won’t be visible, but I always think it’s good practice to build in some

I’d also recommend an obvious extension to this would be to check the file size (perhaps in bytes) and load it in chunks, but I won’t complicate the code with that here.

a. Loading our file in the background with UI updates

Loading text into the programme memory is no different in JavaFX than it would be in plain old Java, so we can do this using a BufferedReader to transcribe the text file’s contents into

private Task<String> fileLoaderTask(File fileToLoad){
    //Create a task to load the file asynchronously
    Task<String> loadFileTask = new Task<>() {
        @Override
        protected String call() throws Exception {
            BufferedReader reader = new BufferedReader(new FileReader(fileToLoad));

            //Use Files.lines() to calculate total lines - used for progress
            long lineCount;
            try (Stream<String> stream = Files.lines(fileToLoad.toPath())) {
                lineCount = stream.count();
            }

            //Load in all lines one by one into a StringBuilder separated by "\n" - compatible with TextArea
            String line;
            StringBuilder totalFile = new StringBuilder();
            long linesLoaded = 0;
            while((line = reader.readLine()) != null) {
                totalFile.append(line);
                totalFile.append("\n");
                updateProgress(++linesLoaded, lineCount);
            }

            return totalFile.toString();
        }
    };

    //If successful, update the text area, display a success message and store the loaded file reference
    loadFileTask.setOnSucceeded(workerStateEvent -> {
        try {
            textArea.setText(loadFileTask.get());
            statusMessage.setText("File loaded: " + fileToLoad.getName());
            loadedFileReference = fileToLoad;
        } catch (InterruptedException | ExecutionException e) {
            Logger.getLogger(getClass().getName()).log(SEVERE, null, e);
            textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath());
        }
    });

    //If unsuccessful, set text area with error message and status message to failed
    loadFileTask.setOnFailed(workerStateEvent -> {
        textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath());
        statusMessage.setText("Failed to load file");
    });

    return loadFileTask;
}

Here, we can use the setOnFailed() method to display helpful error messages to the user. We can also use the setOnSucceeded() method to handle pushing the new content into the TextArea.

4. Bind the Task to our ProgressBar and load the text to the TextArea

Now we have a Task we can bind its progress to the progress displayed in the ProgressBar, and set it running to load our file in the background.

We’ll do all of this in the loadFileToTextArea() method we referenced above.

private void loadFileToTextArea(File fileToLoad) {
    Task<String> loadFileTask = fileLoaderTask(fileToLoad);
    progressBar.progressProperty().bind(loadFileTask.progressProperty());
    loadFileTask.run();
}

How to update the display if the text file changes

There are a few ways to keep track of whether a text file has changed, but by far the easiest is to load the file attributes and check the date and time the file was last modified. To do this in the background we’ll add code in three stages:

  1. Saving the file details and scheduling checking to detect when these change
  2. Using JavaFX events in the ScheduledService to notify the user
  3. Handling whether the user wants to load the changes
JavaFX Application for text editing, diagram detailing the process for detecting changes in text files on the hard drive

1. Saving file details and scheduling checking

We’ll start by saving the time that the file was last modified (lastModifiedTime) as we load the file by adding a line of code to the setOnSucceeded() method of the Task we defined above:

loadFileTask.setOnSucceeded(workerStateEvent -> {
    try {
        textArea.setText(loadFileTask.get());
        statusMessage.setText("File loaded: " + fileToLoad.getName());
        loadedFileReference = fileToLoad;
        lastModifiedTime = Files.readAttributes(fileToLoad.toPath(), BasicFileAttributes.class).lastModifiedTime();
    } catch (InterruptedException | ExecutionException | IOException e) {
        Logger.getLogger(getClass().getName()).log(SEVERE, null, e);
        textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath());
    }
});

Obviously make sure you define lastModifiedTime and loadedFileReference as member variables of the controller :).

private File loadedFileReference;
private FileTime lastModifiedTime;

Now, we can schedule a process to check the time the file was last modified every second. To make sure we keep the UI responsive during the check-and-load cycle, we can run them through a background process that’s scheduled to run periodically.

Scheduling file checking to detect changes

Here, we’ll dig into the JavaFX concurrency package a little to schedule a Task using the ScheduledService object. This is one of a huge number of ways to schedule a Task in JavaFX.

If you’re interested in learning more about your concurrency and scheduling options, and how you can tune your efficiency against your convenience, check out my guide to JavaFX running background processes.

For now, we’ll create a ScheduledService, defining a Task that checks whether the file has been modified since we last loaded it.

private ScheduledService<Boolean> createFileChangesCheckingService(File file){
    ScheduledService<Boolean> scheduledService = new ScheduledService<>() {
        @Override
        protected Task<Boolean> createTask() {
            return new Task<>() {
                @Override
                protected Boolean call() throws Exception {
                    FileTime lastModifiedAsOfNow = Files.readAttributes(file.toPath(), BasicFileAttributes.class).lastModifiedTime();
                    return lastModifiedAsOfNow.compareTo(lastModifiedTime) > 0;
                }
            };
        }
    };
    scheduledService.setPeriod(Duration.seconds(10));
    return scheduledService;
}

We can define the event that will be fired whenever this ScheduledService detects a file change by setting it after it’s created. This event will be fired whenever the Task succeeds.

To get this service running, we’

2. Using JavaFX events to notify the user

Inside the setOnSucceeded() method above, we used a WorkerStateEvent (an event triggered by the status of our task) to fire a method call to notify our user.

Now, we want to define what happens once we’ve detected a change. For a start, we’ll want to stop checking whether the file’s changed, because it already has. Then, we can notify the user and let them decide whether to load the changes.

private void scheduleFileChecking(File file){
    ScheduledService<Boolean> fileChangeCheckingService = createFileChangesCheckingService(file);
    fileChangeCheckingService.setOnSucceeded(workerStateEvent -> {
        //first time task runs, this may be null. Avoid NPE.
        if(fileChangeCheckingService.getLastValue()==null) return;

        if(fileChangeCheckingService.getLastValue()){
            //no need to keep checking
            fileChangeCheckingService.cancel();

            notifyUserOfChanges();
        }
    });
    System.out.println("Starting Checking Service...");
    fileChangeCheckingService.start();
}

Of course, we need to set that going, and a sensible point to start checking is when we initially load the file:

loadFileTask.setOnSucceeded(workerStateEvent -> {
    //beneath the try-catch block:
    scheduleFileChecking(loadedFileReference);
});

With the ScheduledService started, we can now notify the user and await their response!

3. Handling whether the user wants to load the changes

To notify the user of changes to the file, we’ll very simply show the button to he user:

private void notifyUserOfChanges() {
    loadChangesButton.setVisible(true);
}

Now, we need to define the action that will be completed if the user decides to load the changes, which we’ll do in two steps:

  1. Add an action to the button in FXML using the # operator
  2. Create a connected method in the Controller to load the changes based on the file reference we’ve already saved for this file.
FXML:
<Button fx:id="loadChangesButton" onAction="#loadChanges" text="Load Changes" />
Controller:
public void loadChanges(ActionEvent event){
    loadFileToTextArea(loadedFileReference);
    loadChangesButton.setVisible(false);
}

Now, once the user selects the option to re-load the changes, the loadFileToTextArea() will take care of loading the file, and restarting the ScheduledService, which will check for future changes.

How to save TextArea content to a file

Compared to scheduling checks and maintaining a file, saving the contents of our file back into its original location is comparatively simple. Again, the first step is to register an action on the button in the FXML file, and then to create the method in our Controller.

FXML:
<MenuItem text="Save" onAction="#saveFile"/>
Controller:
public void saveFile(ActionEvent event) {
    try {
        FileWriter myWriter = new FileWriter(loadedFileReference);
        myWriter.write(textArea.getText());
        myWriter.close();
        lastModifiedTime = FileTime.fromMillis(System.currentTimeMillis() + 3000);
        System.out.println("Successfully wrote to the file.");
    } catch (IOException e) {
        Logger.getLogger(getClass().getName()).log(SEVERE, null, e);
    }
}

And that’s it! Note I’m also resetting the lastModifiedTime attribute a few seconds into the future so we don’t accidentally pick up the file change in our scheduler. There’s undoubtedly a more elegant event-based way to do this, but for the short example here, it’s absolutely fine.

Full Code Example

In case you want the full, complete code to play with, here it is. You’ll need to adjust the file references based on how you want to store resources in your project, but here’s how I organised mine.

Project Directory
│   pom.xml //I used Maven
│   
└───src
    └───main
        ├───java
        │   │   module-info.java  //I used a modular project
        │   │   
        │   └───com
        │       └───edencoding
        │           │   SimpleFileEditor.java
        │           │   
        │           └───controllers
        │                   SimpleFileEditorController.java
        │                   
        └───resources
            │   
            └───com
                └───edencoding
                    ├───fxml
                    │       SimpleFileEditor.fxml
                    │           
                    └───img
                            EdenCodingIcon.png
                            

Conclusions

It’s actually surprisingly simple to create a text editor in JavaFX.

In this case, we combined the well-defined methods of loading text files in Java with the powerful event-driven mechanics of JavaFX.

At the end, we have a simple, easy to use text editor that can be used to open, sync and save text files on the hard drive.