Wikia

Planet JFX

Developing a File Browser in JavaFX

Talk0
119pages on
this wiki

Developing a File Browser in JavaFXEdit

IntroductionEdit

This document illustrates the development of a simple text file browser implemented in JavaFX. Starting with the basics of creating windows and displaying text the application is incrementally refined until it implements its expected functionality.


It's assumed the reader is familiar with the Java programming language as well as with the basics of JavaFX Script ([Getting Started]).


About File BrowsingEdit

This section describes the expected functionality of our (minimalist) file browser.


A file browser allows the user to select and open text files to be displayed in a window. For this, our example browser presents the user an Open menu option that looks like:


Browser-open

Selecting this menu item causes a file selection dialog to appear:


Browser-open-file

Once a file is selected its contents are displayed in the browser's text area:


Browser-lorem-ipsum

As seen in the above snapshot, the selected file contains long lines that don't fit properly in the window. The Line Wrap item of the View menu causes long lines to be folded like in:


Browser-line-wrap

Here, however, words are truncated along lines in a less than readable manner. The Word Wrap item of the View menu causes truncated words to be wrapped like in:


Browser-word-wrap

Finally, we'd like our application to keep a list of the recently browsed files so that it's possible to return to them without using the file selection dialog:


Browser-recent-files

The remaining of this document presents a step-by-step process in which the described functionality is implemented as an JavaFX script.

Displaying Windows and TextEdit

Before we implement file browsing logic as such, we'll see how to display windows and constant text in JavaFX.


If we want to display a simple window containing fixed text like:


Lorem-ipsum

then the JavaFX code required is:


package browser;
 import javafx.ui.Frame;
 import javafx.ui.RootPane;
 import javafx.ui.TextArea;
 import java.lang.System;
 Frame {
     width: 550
     height: 350
     visible: true
     title: 'Lorem Ipsum'
     onClose: operation() { System.exit(0); }
     content: RootPane {
         content: TextArea {
             text:
 "Lorem ipsum dolor sit amet, consectetur adipisicing elit,
 sed do eiusmod tempor incididunt ut labore et dolore magna
 aliqua. Ut enim ad minim veniam, quis nostrud exercitation
 ullamco laboris nisi ut aliquip ex ea commodo consequat.
 Duis aute irure dolor in reprehenderit in voluptate velit
 esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
 occaecat cupidatat non proident, sunt in culpa qui officia
 deserunt mollit anim id est laborum."
         }
     }
 };

Let's dissect this code.


The package DirectiveEdit

In the statement:

package browser;

the package directive defines the namespace within which the JavaFX compilation unit exists. The notion of package in JavaFX is very close to that of Java.


Unlike Java, though, an JavaFX compilation unit is not limited to defining only one class per source file. JavaFX source files can declare several classes as well as top-level functions, operations and global code and variables. As in all scripting languages, top-level global code is executed inmmediately upon interpretation.


The import DirectiveEdit

import javafx.ui.Frame;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.lang.System;

By convention, classes under the f3 root package correspond to language-supplied classes. This is equivalent to the java root package in the Java programming language.


In general, the import statement is equivalent in meaning to their Java counterpart. For classes to be used without package qualification they must be explicitly imported. Otherwise class names must be fully qualified like in:

<<javafx.ui.Frame>> {
    width: 550
    height: 350
    onClose: operation() { <<java.lang.System>>.exit(0); }
    // snip...
}


Java classes are imported using the same syntax and semantics of JavaFX classes. In general, there's no runtime distinction between JavaFX and Java classes and objects.


Note, however, that Java classes under the java.lang package (such as java.lang.System) still need to explicitly imported; they're not implicitly imported as is the case in Java. Likewise, no f3 package is implicitly imported.

The Applications's Main FrameEdit

Most JavaFX GUI applications declare one Frame object literal that corresponds to the top-level window of the GUI:


Frame {
    width:550
    height: 350
    visible: true
    title: 'Lorem Ipsum'
    onClose: operation() { System.exit(0); }
    content: RootPane {
        content: TextArea {
            text:
"Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum."
        }
    }
};

In the above snippet the width and height attributes determine the size of the window, while the title attribute determines the text used as the window's caption.


The onClose operation is an attribute of type operation(). Class operations are a functional construct allowing the embeding of event handling code in object literal attributes. This simple and readable mechanism replaces the tedious listener or event handler interface implementation required by conventional Swing programming.


Frames require a container as their content. In our case we've chosen a top-level RootPane container. RootPane, in turn, requires one or more JavaFX widgets as its content. In order to keep our example simple, we've chosen a TextArea widget to display our fixed text.


Notice that the string constant containing our sample text spans several lines. Unlike Java, an JavaFX string constant can contain newlines.


JavaFX string constants can also contain embedded expressions that are evaluated at runtime. Such expressions are enclosed in curly braces and can nest:

"The current date is {new <<java.util.Date>>()}."

Adding a MenuEdit

The next step is to add a menu to our application. Since no real browser functionality is yet in place, a File menu will be created with a single action: Exit. The application would then look like:


Lorem-ipsum-menu

The JavaFX code is now:


package browser;
import javafx.ui.Frame;
import javafx.ui.Menu;
import javafx.ui.MenuBar;
import javafx.ui.MenuItem;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.lang.System;
Frame {
    width: 550
    height: 350
    visible: true
    title: 'Lorem Ipsum'
    onClose: operation() { System.exit(0); }
    content: RootPane {
        menubar: MenuBar {
            menus: Menu {
                text: "File"
                mnemonic: F
                items: MenuItem {
                    text: "Exit"
                    mnemonic: X
                    accelerator: {
                        modifier: CTRL
                        keyStroke: Q
                    }
                    action: operation() { System.exit(0); }
                }
            }
        }
        content: TextArea {
            text:
"Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum."
        }
    }
};

A MenuBar has been added as an attribute of the RootPane container. A MenuBar may have one or more Menus each having one or more MenuItems.


In our case we've added a single File menu with a single Exit menu item.


Note that the accelerator keystroke combination is Ctrl-Q instead of Ctrl-X as the menu item mnemonic would suggest. This is due to the fact that, in most environments, Ctrl-X is reserved for the cut clipboard operation.


The action operation attribute is the event handler to be invoked when the menu item is clicked on or is activated via the mnemonic or accelerator keys. As seen, it simply calls System.exit(0) to terminate the application.


A Simple GUI Application PatternEdit

Before we go on to adding more functionality to our application, let's discuss a common pattern recurring in simple JavaFX GUI applications. Understanding this pattern will enable us to add the missing functionality to our application. It's worth noting, however, that this is just a single pattern among many, more complex ones commonly used in JavaFX.


A simple JavaFX GUI application consists of:


A model class whose attributes correspond to data items used by the GUI

A single model class instance referenced and used by the GUI

A Frame object literal embodying the GUI as such Thus, a simplified view of our file browser application would look like:


// The model class...
class BrowserModel {
    attribute fileName: String;
    attribute fileContents: String;
    operation loadFile(file: File);
}
// snip...
// The model instance
var model = new BrowserModel;
// The main GUI frame
Frame {
    width: 550
    height: 350
    visible: true
    title: 'Browser'
    onClose: operation() { System.exit(0); }
    content: RootPane {
        menubar: MenuBar {
            // snip...
        }
        content: TextArea {
            text: bind model.fileContents // The GUI references the model instance
        }
    }
};

For simple applications this pattern occurs once at the script top-level. For more complex applications, though, it is possible to have multiple frames sharing the same or different models.


The Model ClassEdit

The structure of the model class is determined by what data items are used or referenced by the GUI.


In our case, we want the browser to display the file name on its window title. We obviously want to display the file contents, too. Finally, we'll also need a means of loading file contents given a java.io.File object.


Thus, our model class will be:


class BrowserModel {
    attribute fileName: String; // To display in the window caption
    attribute contents: String; // To display in the text area
    operation loadFile(file: File); // To load file contents
}

The implementation of the loadFile operation is straightforward:


operation BrowserModel.loadFile(file: File) {
    var reader = new BufferedReader(new FileReader(file));
    var builder = new StringBuilder();
    while (true) {
        var line = reader.readLine();
        if (line == null) {
            break;
        }
        builder.append(line);
        builder.append('\n');
    }
    reader.close();
    this.fileName = file.canonicalPath;
    this.contents = builder.toString();
}

Note how fileName is assigned from the file's canonicalPath attribute rather than from its getCanonicalPath() method. This is possible because, in JavaFX, Java bean properties can be referenced as JavaFX-style attributes.


The Model InstanceEdit

Instantiating the model instance is trivial


var model = new BrowserModel;

For our simple example, there is only one model instance for the entire GUI application. This single instance is subsequently referenced and used inside the GUI frame in both declarative and procedural ways.


The GUI FrameEdit

The GUI frame is actually an object literal whose nesting structure mirrors that of the GUI visual appearance. For our simple browser the GUI frame is:


Frame {
    var: win
    width: 800
    height: 700
    visible: true
    title: bind "{model.fileName}"
    onClose: operation() { System.exit(0); }
    content: RootPane {
        menubar: MenuBar {
            menus: [
                Menu {
                    text: "File"
                    mnemonic: F
                    items: bind [
                        MenuItem {
                            text: "Open"
                            mnemonic: O
                            accelerator: {
                                modifier: CTRL
                                keyStroke: O
                            }
                            action: operation() {
                                var fc = FileChooser {
                                    action: operation(file: File) {
                                        model.loadFile(file);
                                    }
                                };
                                fc.showOpenDialog(win);
                            }
                        },
                        MenuSeparator {},
                        MenuItem {
                            text: "Exit"
                            mnemonic: X
                            accelerator: {
                                modifier: CTRL
                                keyStroke: Q
                            }
                            action: operation() {
                                System.exit(0);
                            }
                        },
                    ]
                },
            ]
        }
        content: TextArea {
            text: bind model.contents
        }
    }
};

The bind OperatorEdit

bind is a powerful and commonly used JavaFX construct. Let's take a look at the frame's title declaration:


title: bind "{model.fileName}"

This means that the window's caption will contain whatever the value of the "{model.fileName}" expression is. Thus, if the file name is changed programmatically, the window title will be automatically updated without the need to write event handlers or synchronization logic.


In general, when a variable or initializer is bound to an expression its value will automatically change whenever any of the variables participating in the expression changes. This is akin to spreadsheet formula recalculation. Bindings may be declared as lazy in which case recalculation takes place only when the bound variable value is requested.


In our example bind is also used to provide a value for the text area's text content:


content: TextArea {
    text: bind model.contents
}

This is a special case of bind where the right-hand part of the bind is a class attribute rather than an expression. In this case, the binding is bidirectional: any change in model.contents will be automatically reflected in the text area's text attribute and, correspondingly, any change in the text area's text attribute will be automatically reflected in model.contents.


For input-capable widgets such as text fields, text areas or check and radio buttons this is the basic mechanism used to propagate user input to the model's data.

The var pseudo-attributeEdit

Note how the frame object literal has an attribute called var:


Frame {
    var: win
    width: 800
    height: 700
    visible: true
    // snip...
}

This pseudo-attribute does not correspond to any "real" attribute defined for class Frame. Instead, it introduces a local variable that points to the frame object being populated. This variable is visible only inside the frame's object literal and can be used whenever a reference to the object is needed. Of course, the var pseudo-attribute can be used for any object type in an object literal (not just for Frame).


In general, function, operation and variable declarations are allowed between attribute initializers. Such program elements are visible only after their declaration and only inside their enclosing object literal block.


In our example, the frame reference is needed to show the open file dialog, an operation that requires specifying the associated frame. This can be seen in the Open menu item:


MenuItem {
    text: "Open"
    mnemonic: O
    accelerator: {
        modifier: CTRL
        keyStroke: O
    }
    action: operation() {
        var fc = FileChooser {
            action: operation(file: File) {
                model.loadFile(file);
            }
        };
        fc.showOpenDialog(win);
    }
},

Here, the action associated with menu item Open creates a FileChooser widget and displays it specifying the current frame (var: win) as its owner window.


Notice, by the way, that the FileChooser own action is to invoke the loadFile operation on the model instance. This operation changes the value of model.fileName to which the frame title is bound thereby causing the window's caption to be automatically updated.


First Working VersionEdit

We have now a working version of our minimalist file browser. The complete source code is:


import javafx.ui.FileChooser;
import javafx.ui.Frame;
import javafx.ui.Menu;
import javafx.ui.MenuBar;
import javafx.ui.MenuItem;
import javafx.ui.MenuSeparator;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.StringBuilder;
import java.lang.System;
class BrowserModel {
    attribute fileName: String;
    attribute contents: String;
    operation loadFile(file: File);
}
operation BrowserModel.loadFile(file: File) {
    var reader = new BufferedReader(new FileReader(file));
    var builder = new StringBuilder();
    while (true) {
        var line = reader.readLine();
        if (line == null) {
            break;
        }
        builder.append(line);
        builder.append('\n');
    }
    reader.close();
    this.fileName = file.canonicalPath;
    this.contents = builder.toString();
}
var model = new BrowserModel;
Frame {
    var: win
    width: 550
    height: 350
    visible: true
    title: bind "{model.fileName}"
    onClose: operation() { System.exit(0); }
    content: RootPane {
        menubar: MenuBar {
            menus: [
                Menu {
                    text: "File"
                    mnemonic: F
                    items: bind [
                        MenuItem {
                            text: "Open"
                            mnemonic: O
                            accelerator: {
                                modifier: CTRL
                                keyStroke: O
                            }
                            action: operation() {
                                var fc = FileChooser {
                                    action: operation(file: File) {
                                        model.loadFile(file);
                                    }
                                };
                                fc.showOpenDialog(win);
                            }
                        },
                        MenuSeparator {},
                        MenuItem {
                            text: "Exit"
                            mnemonic: X
                            accelerator: {
                                modifier: CTRL
                                keyStroke: Q
                            }
                            action: operation() {
                                System.exit(0);
                            }
                        },
                    ]
                },
            ]
        }
        content: TextArea {
            text: bind model.contents
        }
    }
};

The initial, empty window will look like:


Browser-empty

We have added a menu separator between the Open and Exit menu items. Thus, upon activating the menu, the window will look like:


Browser-menu

After selecting the Open menu item, a file chooser dialog is displayed that allows the user to select what file to browse:


Browser-choose

Once the file has been selected its contents are displayed in the frame's text area:


Browser-content

A Few ImprovementsEdit

We now introduce some improvements to our file browser application.


Exiting the ApplicationEdit

Note that the onClose frame attribute and the Exit menu item both invoke System.exit(0). This redundancy becomes inconvenient when it's necessary to execute wrapup logic prior to exiting the application. Such logic is better centralized in a single location. Thus, a better way to structure this is to add an exit() operation to the BrowserModel class as follows:


class BrowserModel {
    attribute fileName: String;
    attribute contents: String;
    operation openFile(file: File);
    operation exit();
}
operation BrowserModel.exit() {
    // Any wrapup logic would go here
    System.exit(0);
}

Given this operation, the event handler and the menu item can now simply invoke the exit() operation on the model instance:


Frame {
    var: win
    width: 800
    height: 700
    visible: true
    onClose: operation() { model.exit(); }
    // Snip...
        MenuItem {
            text: "Exit"
            mnemonic: X
            accelerator: {
                modifier: CTRL
                keyStroke: Q
            }
            action: operation() { model.exit(); }
        },
    // Snip..
};

Avoiding Content ModificationEdit

By default, a TextArea allows user input on its text content. Since our application is a read-only browser -not an editor- the user may be confused because she would be able to modify text and, therefore, would also expect to be able to save changes. In order to avoid such confusion it's necessary to disable user input:


content: TextArea {
    editable: false
    text: bind model.contents
}

Showing the Application TitleEdit

It's customary for browsers to have a window caption that shows the name of the application plus the name of the file being displayed. For this we modify our title frame property as follows:


Frame {
    var: win
    width: 550
    height: 350
    visible: true
    onClose: operation() { model.exit(); }
    function makeTitle(fileName: String) =

        "JavaFX Browser {if fileName <> null and fileName.length() <> 0 then " - {fileName}" else ''}";
    title: bind makeTitle(model.fileName)
    // Snip...
};

Notice that the declaration of function makeTitle is inlined in the Frame object literal. This is another case in which an inline declaration is made between property initializers. As usual, the declared function is visible only after its declaration and only inside its enclosing object literal block.


In our case, function makeTitle is declared as a simple string expression that generates the application name (JavaFX Browser) plus an optional dash and the file name if it's set.


Despite its procedural appearance, the if a then b else c expression is actually equivalent to Java's a ? b : c. Thus, inside expressions, if is a ternary operator, not a flow control directive.


It's also worth noting that in the title string expression there is a nested substitution expression ({filename}).


Revised CodeEdit

The following program listing shows our code after adding these improvements. Changes introduced with respect to the previous version are shown in bold.


package browser;
import javafx.ui.FileChooser;
import javafx.ui.Frame;
import javafx.ui.Menu;
import javafx.ui.MenuBar;
import javafx.ui.MenuItem;
import javafx.ui.MenuSeparator;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.StringBuilder;
import java.lang.System;
class BrowserModel {
    attribute fileName: String;
    attribute contents: String;
    operation loadFile(file: File);
    operation exit();
}
operation BrowserModel.exit() {
    System.exit(0);
}
operation BrowserModel.loadFile(file: File) {
    var reader = new BufferedReader(new FileReader(file));
    var builder = new StringBuilder();
    while (true) {
        var line = reader.readLine();
        if (line == null) {
            break;
        }
        builder.append(line);
        builder.append('\n');
    }
    reader.close();
    this.fileName = file.canonicalPath;
    this.contents = builder.toString();
}
var model = new BrowserModel;
Frame {
    var: win
    width: 800
    height: 700
    visible: true
    onClose: operation() { model.exit(); }
    function makeTitle(fileName: String) =

        "Browser {if fileName <> null and fileName.length() <> 0 then " - {fileName}" else ''}";
    title: bind makeTitle(model.fileName)
    content: RootPane {
        menubar: MenuBar {
            menus: [
                Menu {
                    text: "File"
                    mnemonic: F
                    items: bind [
                        MenuItem {
                            text: "Open"
                            mnemonic: O
                            accelerator: {
                                modifier: CTRL
                                keyStroke: O
                            }
                            action: operation() {
                                var fc = FileChooser {
                                    action: operation(file: File) {
                                        model.loadFile(file);
                                    }
                                };
                                fc.showOpenDialog(win);
                            }
                        },
                        MenuSeparator {},
                        MenuItem {
                            text: "Exit"
                            mnemonic: X
                            accelerator: {
                                modifier: CTRL
                                keyStroke: Q
                            }
                            action: operation() {
                                model.exit();
                            }
                        },
                    ]
                },
            ]
        }
        content: TextArea {
            editable: false
            text: bind model.contents
        }
    }
};

This is a good moment to ponder how simple and expressive JavaFX is for building GUI's. Our working file browser weighs a mere 100 lines of code practically all of which is declarative. Contrast this with an equivalent Swing application.


Adding a View MenuEdit

A View menu provides options to control the display of file contents inside the text area:


Browser-view-menu

Adding a view menu involves extending class BrowserModel to add attributes corresponding to the word wrap and line wrap features of TextArea:


class BrowserModel {
    attribute fileName: String;
    attribute contents: String;
    attribute wordWrap: Boolean;
    attribute lineWrap: Boolean;
    operation loadFile(file: File);
    operation exit();
}

Correspondingly, the TextArea object literal must be extended to bind to these new attributes.


content: TextArea {
    editable: false
    wrapStyleWord: bind model.wordWrap
    lineWrap: bind model.lineWrap
    text: bind model.contents
}

Finally, a new menu must be added to allow the user to control the wrapping settings:


Menu {
    text: "View"
    mnemonic: E
    items: [
        CheckBoxMenuItem {
            text: "Word Wrap"
            mnemonic: W
            selected: bind model.wordWrap
        },
        CheckBoxMenuItem {
            text: "Line Wrap"
            mnemonic: L
            selected: bind model.lineWrap
        },
    ]
},

It's interesting to see how bind works behind the scenes to synchronize program state. Let's consider the following scenario:


Line-wrap-binding

The user checks the menu item labeled Line Wrap. This changes the menu item's selected attribute from false to true. A check mark is placed to the left of the menu item's label.

The menu item's selected attribute is bound to the model instance lineWrap attribute in a bidirectional fashion. Therefore when the value of selected changes in response to user input, the value of attribute lineWrap in the model instance changes also to true.

The attribute lineWrap of the browser's TextArea is, in turn, bidirectionally bound to the model instance lineWrap attribute. Thus, when the model instance attribute changes in response to the menu item change, the change propagates to the lineWrap attribute of the text area.

The change in attribute lineWrap in the text area eventually propagates to the underlying Swing JTextArea component. This results in an immediate visual change where long lines are folded. Contrast this with conventional Swing programming where procedural event handlers must be written to handle and propagate state change.


The following program listing shows our code after adding the view menu. Changes introduced with respect to the previous version are shown in bold.


package browser;
import javafx.ui.CheckBoxMenuItem;
import javafx.ui.FileChooser;
import javafx.ui.Frame;
import javafx.ui.Menu;
import javafx.ui.MenuBar;
import javafx.ui.MenuItem;
import javafx.ui.MenuSeparator;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.StringBuilder;
import java.lang.System;
class BrowserModel {
    attribute fileName: String;
    attribute contents: String;
    attribute wordWrap: Boolean;
    attribute lineWrap: Boolean;
    operation loadFile(file: File);
    operation exit();
}
operation BrowserModel.exit() {
    System.exit(0);
}
operation BrowserModel.loadFile(file: File) {
    var reader = new BufferedReader(new FileReader(file));
    var builder = new StringBuilder();
    while (true) {
        var line = reader.readLine();
        if (line == null) {
            break;
        }
        builder.append(line);
        builder.append('\n');
    }
    reader.close();
    this.fileName = file.canonicalPath;
    this.contents = builder.toString();
}
var model = new BrowserModel;
Frame {
    var: win
    width: 800
    height: 700
    visible: true
    onClose: operation() { model.exit(); }
    function makeTitle(fileName: String) =

        "Browser {if fileName <> null and fileName.length() <> 0 then " - {fileName}" else ''}";
    title: bind makeTitle(model.fileName)
    content: RootPane {
        menubar: MenuBar {
            menus: [
                Menu {
                    text: "File"
                    mnemonic: F
                    items: bind [
                        MenuItem {
                            text: "Open"
                            mnemonic: O
                            accelerator: {
                                modifier: CTRL
                                keyStroke: O
                            }
                            action: operation() {
                                var fc = FileChooser {
                                    action: operation(file: File) {
                                        model.loadFile(file);
                                    }
                                };
                                fc.showOpenDialog(win);
                            }
                        },
                        MenuSeparator {},
                        MenuItem {
                            text: "Exit"
                            mnemonic: X
                            accelerator: {
                                modifier: CTRL
                                keyStroke: Q
                            }
                            action: operation() {
                                model.exit();
                            }
                        },
                    ]
                },
                Menu {
                    text: "View"
                    mnemonic: E
                    items: [
                        CheckBoxMenuItem {
                            text: "Word Wrap"
                            mnemonic: W
                            selected: bind model.wordWrap
                        },
                        CheckBoxMenuItem {
                            text: "Line Wrap"
                            mnemonic: L
                            selected: bind model.lineWrap
                        },
                    ]
                },
            ]
        }
        content: TextArea {
            editable: false
            wrapStyleWord: bind model.wordWrap
            lineWrap: bind model.lineWrap
            text: bind model.contents
        }
    }
};

Adding the view menu fattened our code size to 121 lines, 99% of which correspond to declarative, non-procedural code. Notice also how the containment structure of this declarative code mirrors the visual appearance and the widget composition of the GUI.


Adding a List of Recently Opened FilesEdit

Our last goal is to maintain a dynamic set of menu items under the File menu where each menu item corresponds to a previously viewed file:


Browser-recent-files

To maintain a list of recently opened files we extend the model class to add a recentFileNames array attribute.


class BrowserModel {
    attribute fileName: String;
    attribute fileContents: String;
    attribute recentFileNames: String*;
    attribute lineWrap: Boolean;
    attribute wordWrap: Boolean;
    operation loadFile(file: File);
    operation exit();
}

A file name is added to the list of recently opened files whenever a new file is opened and the previous one abandoned. The context appropriate to capture this event is in the fileName attribute value change trigger:


attribute BrowserModel.fileName = null;
trigger on BrowserModel.fileName[oldValue] = newValue {
    var i:Number;
    if (oldValue <> null and oldValue <> newValue) {
        delete recentFileNames[i | i == newValue];
        insert oldValue as first into recentFileNames;
        delete recentFileNames[i | indexof i > 8];
    }
}

Here, we initialize the fileName attribute value to null implying no file has been opened so far.


Whenever a file is loaded the value of the fileName attribute changes and the above trigger fires.


The fileName TriggerEdit

The aliases chosen for the previous and current value of fileName are oldValue and newValue respectively.


The logic inside trigger on fileName change executes only if:


At least one file has been previously opened. This is the case when fileName is not null.

The new file being opened is not the same currently open file

if (oldValue <> null and oldValue <> newValue)

If these conditions hold, the first step is to remove the current file from the list of recently viewed files. This prevents the file from appearing in the menu more than once when it has been visited several times:


delete recentFileNames[. == newValue];

In the list of recently viewed files the most recent one will be the first in the menu list. For this reason, its name must be added to the recently viewed file array as first:


insert oldValue as first into recentFileNames;

If the resulting array has more than 9 elements the excess element is deleted in a least-recently-used fashion:


delete recentFileNames[indexof . > 8];

The reason why we limit the size of the recently viewed files to 9 elements is that we intend to use keystrokes _1 through _9 as file menu mnemonics.


The Dynamic MenuEdit

The following listing shows how the File menu has been extended to include a dynamic list of file names:


import javafx.ui.KeyStroke;
// Snip...
Menu {
    var keyStrokes:KeyStroke = [_1,_2,_3,_4,_5,_6,_7,_8,_9]
    text: "File"
    mnemonic: F
    items: bind [
        MenuItem {
            text: "Open"
            mnemonic: O
            accelerator: {
                modifier: CTRL
                keyStroke: O
            }
            action: operation() {
                var fc = FileChooser {
                    action: operation(file: File) {
                        model.loadFile(file);
                    }
                };
                fc.showOpenDialog(win);
            }
        },
        if sizeof model.recentFileNames > 0 
        then MenuSeparator {} 
        else [],
        foreach (f in model.recentFileNames)
            MenuItem {
                mnemonic: bind keyStrokes[indexof f]
                text: bind "{indexof f + 1} {f}"
                action: operation() {
                    model.loadFile(new File(f));       
                }
            },
        MenuSeparator {},
        MenuItem {
            text: "Exit"
            mnemonic: X
            accelerator: {
                modifier: CTRL
                keyStroke: Q
            }
            action: operation() { model.exit(); }
        },
    ]
},

A MenuSeparator is inserted only if the model instance's recentFileNames is not empty. This is true only when at least 2 files have been opened:


if sizeof model.recentFileNames > 0 
then MenuSeparator {} 
else [],

Note how object literals can be populated conditionally. Here, the MenuSeparator may appear or disappear subject to a runtime condition. Conditional object literal fragments may affect the GUI structure or appearance in response to changes to the value of variables used in a conditional expression.


The key to dynamically generating menu items is JavaFX's powerful foreach construct:


foreach (f in model.recentFileNames)
    MenuItem {
        mnemonic: bind keyStrokes[indexof f]
        text: bind "{indexof f + 1} {f}"
        action: operation() {
            model.loadFile(new File(f));       
        }
    },

foreach traverses an array and yields another array each of whose elements is built from its body template. In this case, the template to be expanded on each iteration is a MenuItem object literal. In our example, the iteration variable to be referenced inside the template is f.


Notice that the mnemonic and text attributes of each MenuItem are bound to (rather than just assigned from) expressions involving the iteration variable f. This ensures that whenever the model.recentFileNames array changes the whole collection of menu items is rebuilt.


model.recentFileNames changes inside the BrowserModel.fileName trigger presented above. Attribute Browser.fileName, in turn, changes whenever a new file is loaded. The net effect is that if a new file is selected for viewing then the File menu is dyamically rebuilt to include the new list of recently viewed files. All this without intervening procedural logic.


The Complete ApplicationEdit

In 147 lines of declarative code our application has now achieved its intended functionality. The final code is:


package browser;
import javafx.ui.CheckBoxMenuItem;
import javafx.ui.FileChooser;
import javafx.ui.Frame;
import javafx.ui.KeyStroke;
import javafx.ui.Menu;
import javafx.ui.MenuBar;
import javafx.ui.MenuItem;
import javafx.ui.MenuSeparator;
import javafx.ui.RootPane;
import javafx.ui.TextArea;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.StringBuilder;
import java.lang.System;
class BrowserModel {
    attribute fileName: String;
    attribute fileContents: String;
    attribute recentFileNames: String*;
    attribute lineWrap: Boolean;
    attribute wordWrap: Boolean;
    operation loadFile(file: File);
    operation exit();
}
attribute BrowserModel.fileName = null;
trigger on BrowserModel.fileName[oldValue] = newValue {
    if (oldValue <> null and oldValue <> newValue) {
        delete recentFileNames[. == newValue];
        insert oldValue as first into recentFileNames;
        delete recentFileNames[indexof . > 8];
    }
}
operation BrowserModel.exit() {
    System.exit(0);
}
operation BrowserModel.loadFile(file: File) {
    var reader = new BufferedReader(new FileReader(file));
    var builder = new StringBuilder();
    while (true) {
        var line = reader.readLine();
        if (line == null) {
            break;
        }
        builder.append(line);
        builder.append('\n');
    }
    reader.close();
    this.fileName = file.canonicalPath;
    this.fileContents = builder.toString();
}
var model = new BrowserModel;
Frame {
    var: win
    width: 550
    height: 350
    visible: true
    onClose: operation() { model.exit(); }
    function makeTitle(fileName: String) =

        "JavaFX Browser {if fileName.length() <> 0 then " - {fileName}" else ''}";
    title: bind makeTitle(model.fileName)
    content: RootPane {
        menubar: MenuBar {
            menus: [
                Menu {
                    var keyStrokes:KeyStroke = [_1,_2,_3,_4,_5,_6,_7,_8,_9]
                    text: "File"
                    mnemonic: F
                    items: bind [
                        MenuItem {
                            text: "Open"
                            mnemonic: O
                            accelerator: {
                                modifier: CTRL
                                keyStroke: O
                            }
                            action: operation() {
                                var fc = FileChooser {
                                    action: operation(file: File) {
                                        model.loadFile(file);
                                    }
                                };
                                fc.showOpenDialog(win);
                            }
                        },
                        if sizeof model.recentFileNames > 0 
                        then MenuSeparator {} 
                        else [],
                        foreach (f in model.recentFileNames)
                            MenuItem {
                                mnemonic: bind keyStrokes[indexof f]
                                text: bind "{indexof f + 1} {f}"
                                action: operation() {
                                    model.loadFile(new File(f));       
                                }
                            },
                        MenuSeparator {},
                        MenuItem {
                            text: "Exit"
                            mnemonic: X
                            accelerator: {
                                modifier: CTRL
                                keyStroke: Q
                            }
                            action: operation() { model.exit(); }
                        },
                    ]
                },
                Menu {
                    text: "View"
                    mnemonic: E
                    items: [
                        CheckBoxMenuItem {
                            text: "Word Wrap"
                            mnemonic: W
                            selected: bind model.wordWrap
                        },
                        CheckBoxMenuItem {
                            text: "Line Wrap"
                            mnemonic: L
                            selected: bind model.lineWrap
                        },
                    ]
                },
            ]
        }
        content: TextArea {
            wrapStyleWord: bind model.wordWrap
            lineWrap: bind model.lineWrap
            text: bind model.fileContents
        }
    }
};

Around Wikia's network

Random Wiki