"

75 Javanotes 9.0, Section 13.4 — Mostly Windows and Dialogs

Section 13.4

Mostly Windows and Dialogs



All of the sample GUI programs
that we have looked
at have used a single window. However, many real programs use multiple windows.
In this section, we look at how to manage a multi-window application.
We will also discuss dialog boxes—small popup windows that are mainly
used for getting input from the user. As a bonus, you get to learn about
WebView, a JavaFX control that implements much of the
functionality of a web browser window.


13.4.1  Dialog Boxes

A dialog, or dialog box, is a window that is dependent
on another window. That window is the “parent” or “owner” of the dialog
box. If the parent window is closed, the dialog box is automatically
closed as well.

A dialog box can be modal or modeless.
When a modal dialog box is opened, its parent window is blocked. That is,
the user cannot interact with the parent window until the dialog box is
closed. There are also application modal dialog boxes,
which block the entire application, not just one parent window. Many
dialog boxes in JavaFX are application modal.
Modal dialog boxes are often popped up during program execution
to get input from the user or sometimes just to show a message to the
user.

Modeless dialog boxes do not block interaction with their
parent windows, but they will be closed automatically when the parent
window closes. A modeless dialog box might be used to show an alternative
view of data from the parent window, or to hold some controls that
affect the window.

It is possible to make a Stage work as a dialog box,
but many dialog boxes in JavaFX programs are created as
objects belonging to the class Dialog, from
package javafx.scene.control, or to one of its subclasses.
A Dialog, dlg, has two instance methods for showing
the dialog: dlg.show() and dlg.showAndWait().
If the dialog is shown using dlg.showAndWait(), then it is
modal. (A dialog opened using showAndWait() is not just
modal; it is application modal.)
The showAndWait() method does not return until
the dialog box has been closed, so that any input from the dialog box
will be available to the program immediately after the call to
showAndWait(). If the dialog is shown using dlg.show(),
on the other hand, the dialog is modeless. The show() method
returns immediately, and the user can then use both regular windows and
the dialog box, and can switch back and forth between them. Using a
modeless dialog box is a little like parallel programming—you
have to understand that two things are going on at the same time.
We will consider only modal dialog boxes here.


Dialog<T> is a parameterized type. The type
parameter represents the type of value that will be returned by the
showAndWait() method. The return type is actually
Optional<T>, representing a value of type
T that might or might not be present.
Optional is defined in package java.util.
An Optional has a boolean method
isPresent() to test whether the value is present, and
a method get() that returns the value if one is present.
An exception occurs if get() is called when no value is
present. This just means that if you want to use the return value
from showAndWait(), you should first use isPresent()
to test whether a value was actually returned.

A dialog box will ordinarily contain
one or more buttons for closing the dialog. Typical button names include
“OK”, “Cancel”, “Yes”, and “No”. The most common buttons are
represented by the enumerated type ButtonType,
which has values including ButtonType.OK,
ButtonType.CANCEL, ButtonType.YES,
and ButtonType.NO. ButtonType
is a common return type for a Dialog,
representing the button that the user clicked to close the dialog.
In that case, the dialog box is of type Dialog<ButtonType>.

The class Alert is a subclass of Dialog<ButtonType>
that makes it easy to create dialogs that show a text message to the user, along
with one or two buttons. This class
was already used in Subsection 11.2.3, without much explanation,
to show error messages to the user. An alert can be created with

Alert alert = new Alert( alertType, message );

The first parameter is of type Alert.AlertType which
is a nested enumerated type with values including Alert.AlertType.INFORMATION,
Alert.AlertType.WARNING, Alert.AlertType.ERROR, and
Alert.AlertType.CONFIRMATION. Alerts of the first three types
will have a single “OK” button and do nothing but show the message to the user;
for these alerts, there is no reason to check the return value of alert.showAndWait().
A confirmation alert has an “OK” button and a “Cancel” button and is typically used
to ask whether the user would like to continue with some potentially dangerous operation
such as deleting a file; in this case, checking the return value is important.
Here is a typical use:

Alert confirm = new Alert( Alert.AlertType.CONFIRMATION,
                         "Do you really want to delete " + file.getName() );
Optional<ButtonType> response = confirm.showAndWait();
if ( response.isPresent() && response.get() == ButtonType.OK ) {
    file.delete();
}

In addition to buttons, a Dialog can have:
a content area;
header text that appears above the content area; a graphic
that appears next to the header text, if there is any, or next to the
content; and of course a title in the title bar of the dialog window.
Usually the graphic, if any, would be a small icon image.
For an Alert, the message goes in the content area.
The other properties are set automatically, depending on the alert type,
but they can be changed by calling methods from the Dialog
class before showing the alert:

alert.setTitle( windowTitle );
alert.setGraphic( node );
alert.setHeaderText( headerText );

Any of the values can be null. The content can be set to an arbitrary
scene graph node, replacing the text of the message, by calling

alert.getDialogPane().setContent( node );

but it would be more common to do that for a plain Dialog
than for an Alert. Here are a couple of confirmation
alerts, showing the various components of the dialog window. For the
dialog box on the right, the header text is null. Note, by the way, that to
get multiline text in an alert, you have to include line feed (“\n”) characters
in the text.

confirmation dialog boxes

For an example of a dialog that gets input from the user,
the class TextInputDialog is a subclass
of Dialog<String>, meaning that the return
value of showAndWait() will be Optional<String>.
A TextInputDialog
contains a TextField where the user can enter a line
of text, and it has an “OK” button and a “Cancel” button. The constructor
has a parameter of type String that represents the
initial content of the text input box. If you want to ask a question or
show some message to the user, you can put it into the dialog header text.
The return value, if present, will be the content of the input box.
Note that the return value can be the empty string.
If the user clicks “Cancel” or simply closes the dialog box, then the
return value is not present. Here is a typical usage:

TextInputDialog getNameDialog = new TextInputBox("Fred");
getNameDialog.setHeaderText("Please enter your name.");
Optional<String> response = getNameDialog.showAndWait();
if (response.isPresent() && response.get().trim().length() > 0) {
    name = response.get().trim();
}
else {
    Alert error = new Alert( Alert.AlertType.ERROR,
                               "Anonymous users are not allowed!" );
    error.showAndWait();
    System.exit(1):
}

Since I find Alert and TextInputDialog
a little cumbersome to use (especially since I tend to prefer dialogs without icons),
I wrote my own utility class, SimpleDialogs.java, that defines several more
convenient static methods for showing some common kinds of dialog. In particular,

  • SimpleDialogs.message(text) — shows the text and
    an OK button. There is no return value. The text will be automatically wrapped,
    so that line feed characters are not needed in long messages. An optional
    second parameter can be included to specify the title for the dialog window.
  • SimpleDialogs.prompt(text) — shows the text and
    a text input box, with an OK button and a Cancel button. This method returns a
    String, which will be the content of the input box
    if the user clicks OK, or will be null if the user cancels the dialog.
    The window title can be given as an optional second parameter, and the
    initial content of the text input box as an optional third parameter.
  • SimpleDialogs.confirm(text) — shows the text
    along with “Yes”, “No”, and “Cancel” buttons. The return value is a
    String which will always be one of “yes”, “no”,
    or “cancel”. Again, the window title can be given as an optional
    second parameter.

There are some other options, including a basic color chooser dialog,
which you can learn about by reading the source code.
The program TestDialogs.java lets the user try out the
dialogs that are defined in SimpleDialogs.


13.4.2  WebView and WebEngine

In the rest of this section, we will look at a multi-window web browser
program. Writing a web browser sounds complicated, and it is, but
JavaFX makes it pretty easy, by doing most of the work in a couple of
standard classes. The WebView class,
in package javafx.scene.control, represents a control
that can load and display a web page. It handles most web pages
pretty well, including running JavaScript code. (JavaScript is
the programming language that is used to program active web pages;
it is not closely related to Java.) A WebView
is basically the “view” part of the Model-View-Controller pattern
(Subsection 13.3.2).
The actual work of loading and managing the web page is done by
an object of type WebEngine that is part
of the “controller.” The “model” would be a data structure
containing the content that is displayed in the web page.
The model is created by the WebEngine
when a page is loaded, and the WebView
then displays the content.

A WebView must be placed into a window.
The sample class BrowserWindow.java is
a subclass of the standard window class, Stage, that represents a
complete web browser window. A BrowserWindow contains
a WebView along with a menu bar
and some other controls. In particular, there is a text-input
box where the user can type the URL for a web page and a “Load”
button that the user can click to load the web page from that
URL into the WebView. In addition,
the BrowserWindow constructor can specify
an initial URL to be loaded when the window is first opened.

A WebView has an associated WebEngine
that can be obtained by calling webEngine = webview.getEngine(). Then, to
load a web page, you can simply call

webEngine.load( urlString );

where urlString is a string containing the URL. (URLs were
discussed in Subsection 11.4.1.) The urlString must start
with a “protocol” such as “http:” or “https:”; in my program, I add “http://”
to the front of the URL string, if the string does not already specify a
protocol.

A new web page will also be loaded automatically if the user clicks a link
on the page that is currently being displayed.

Web page loading is asynchronous. That is, webEngine.load() returns
immediately, and the web page is loaded in a background thread. When the
loading has completed, the web page is shown in the WebView.
If the load fails for some reason, there is no automatic notification. However,
you can get some information about what is going on by adding listeners to
two observable String properties of the web engine:
the location and the title. The location is the URL of the
web page that is currently being displayed or loaded, while the title is the
title of the current web page, which usually appears in the title bar of the window
that displays the web page. For example, the BrowserWindow class
monitors the title property and sets its window title to match:

webEngine.titleProperty().addListener( (o,oldVal,newVal) -> {
    if (newVal == null)
        setTitle("Untitled " + owner.getNextUntitledCount());
    else
        setTitle(newVal); 
});

(I will discuss the “owner” below.) It also monitors the location
property and displays its value in a Label at the bottom of the
window.

To monitor the progress of loads, you can also add a listener to the property
webEngine.getLoadWorker().stateProperty(); see the
BrowserWindow.java source code for an example.

I said above that a WebView (with its WebEngine)
can run JavaScript code that occurs on web pages. That is not quite true. JavaScript
has subroutines for popping up certain simple dialog boxes: an “alert” dialog to simply display a message
to the user; a “prompt” dialog to ask the user a question and get back a response string;
and a “confirm” dialog that shows a message with an “OK” and a “Cancel” button.
For a confirm dialog, the return value is a boolean that tells whether the user dismissed
the dialog by clicking “OK”. By default, requests from JavaScript to show these
dialogs are ignored by the WebEngine. However, it is possible to add event handlers to the
web engine to respond to those requests. In BrowserWindow,
I use dialog boxes from my SimpleDialogs class to
respond to the events. For example, when JavaScript tries
to pop up an alert dialog, the web engine generates an event of type
AlertEvent. The data in the event object is the message that JavaScript wants
to display. The BrowserWindow class responds by
using SimpleDialogs.message() to display the message to the user:

webEngine.setOnAlert( 
        evt -> SimpleDialogs.message(evt.getData(), "Alert from web page") );

Handling prompt and confirm dialogs is a little different, since they must
return a value. Here is what’s done in the sample program:

webEngine.setPromptHandler( promptData -> 
            SimpleDialogs.prompt( promptData.getMessage(), 
                      "Query from web page", promptData.getDefaultValue() ) );
webEngine.setConfirmHandler( str -> 
            SimpleDialogs.confirm(str, "Confirmation Needed").equals("yes") );

I haven’t yet discussed the menu bar for a BrowserWindow.
The menu bar contains a single menu, named “Window”. That menu contains commands for
opening new browser windows and for closing the current window. It also contains
a list of browser windows that are currently open. The user can bring a different
window to the front of the screen by selecting the window from that list.
To understand how that works, you need to understand how BrowserWindow
is used in a complete, multi-window program.


13.4.3  Managing Multiple Windows

The class BrowserWindow is not an Application.
It cannot be run as a program; it represents just one window in a multi-window program.
The executable class for the program is defined in WebBrowser.java.
The class WebBrowser,
like any JavaFX program, is a subclass of Application. It depends on
BrowserWindow.java and SimpleDialogs.java, so you
need all three Java files to run the program.

An Application has a start() method that is called by
the system when the application begins. The method has a parameter of type Stage
that represents the “primary window” for the program, but there is no requirement that the
program actually use that window. The start() method in WebBrowser
ignores the primary window and instead creates and shows a window of type BrowserWindow.
That is the first window opened when the program is run. It is set to load the front page
of the web version of this very textbook.

That could have been everything that WebBrowser.java needs to do — except for the
“Window” menu, which contains a list of all open windows. That list is not part of
the data for an individual window, so it has to kept somewhere else. In the web browser
application, the application object, of type WebBrowser, maintains the list of open
windows. There is only one application object in the program, so we have just one list of open windows.
(Another possibility would have been to make the window list a static
member variable in the BrowserWindow class, since static
variables in a class are shared by all instances of the class.) The WebBrowser
class has a newBrowserWindow() method for opening new windows. A BrowserWindow
has an instance variable, owner, that refers to the WebBrowser application
that opened the window. When the browser window wants to open a new window, it does so by
calling owner.newBrowserWindow(url), where the parameter, url,
is the URL of the web site to be loaded by the new window, or is null to
open an empty browser window.

By default in JavaFX, the size of a window is determined by the size of the Scene
that it contains, and the window is centered on the screen. However, it is possible to set the size
and location of a window before it is opened. For a multi-window program, it is not desirable for
all of the windows to appear in exactly the same location. And it turns out that the default size
for a BrowserWindow is probably too small for most computer screens.
In the WebBrowser application, each window that is opened is offset a
little from the location where the previous window was opened, and the size of the window depends
on the size of the screen.

The class Screen, in package javafx.stage
has a static method Screen.getPrimary() that returns an object containing information
about the computer’s main screen. And that object in turn has a method
Screen.getPrimary().getVisualBounds() that returns a Rectangle2D
representing the usable area of the main screen. This is used in the program’s
start() method to compute a size and location for the first window:

public void start(Stage stage) {  // (stage is not used)
    
    openWindows = new ArrayList<BrowserWindow>();  // List of open windows.
    
    screenRect = Screen.getPrimary().getVisualBounds();
    
       // (locationX,locationY) will be the location of the upper left
       // corner of the next window to be opened.  For the first window,
       // the window is moved a little down and over from the top-left
       // corner of the primary screen's visible bounds.
    locationX = screenRect.getMinX() + 30;
    locationY = screenRect.getMinY() + 20;
    
       // The window size depends on the height and width of the screen's
       // visual bounds, allowing some extra space so that it will be
       // possible to stack several windows, each displaced from the
       // previous one.  (For aesthetic reasons, limit the width to be
       // at most 1.6 times the height.)
    windowHeight = screenRect.getHeight() - 160;
    windowWidth = screenRect.getWidth() - 130;
    if (windowWidth > windowHeight*1.6)
        windowWidth = windowHeight*1.6;
    
       // Open the first window, showing the front page of this textbook.
    newBrowserWindow("https://math.hws.edu/javanotes/index.html");
    
} // end start()

When a window is opened in the newBrowserWindow() method,
its size and location are taken from the variables windowWidth,
windowHeight, locationX, and locationY.
And the values of locationX and locationY are
modified so that the next window will be placed at a different location.
In addition, the new window is added to the open window list. We also
have to make sure that the window is removed from that list when it is
closed. Fortunately, a window generates an event when it is closed.
We can add an event handler to listen for that event, and the event
handler can remove the window from the open window list. Here is
the code for newBrowserWindow():

void newBrowserWindow(String url) {
    BrowserWindow window = new BrowserWindow(this,url);
    openWindows.add(window); // Add new window to open window list.
    window.setOnHidden( e -> {
            // Called when the window has closed.  Remove the window
            // from the list of open windows.
        openWindows.remove( window );
        System.out.println("Number of open windows is " + openWindows.size());
        if (openWindows.size() == 0) {
               // Program ends automatically when all windows have been closed.
            System.out.println("Program ends because all windows are closed");
        }
    });
    if (url == null) {
        window.setTitle("Untitled " + getNextUntitledCount());
    }
    window.setX(locationX);         // set location and size of the window
    window.setY(locationY);
    window.setWidth(windowWidth);
    window.setHeight(windowHeight);
    window.show();
    locationX += 30;    // set up location for NEXT window
    locationY += 20;
    if (locationX + windowWidth + 10 > screenRect.getMaxX()) {
            // Window would extend past the right edge of the screen,
            // so reset locationX to its original value.
        locationX = screenRect.getMinX() + 30;
    }
    if (locationY + windowHeight + 10 > screenRect.getMaxY()) {
            // Window would extend past the bottom edge of the screen,
            // so reset locationY to its original value.
        locationY = screenRect.getMinY() + 20;
    }
}

The WebBrowser class has a method
getOpenWindowList() that returns the open window list.
This method is used by a BrowserWindow when
it constructs the “Window” menu. This is not done in a very efficient
way: The menu is rebuilt each time it is shown. A menu emits an
event when the user clicks the menu name, just before the menu is
shown. The BrowserWindow registers a
handler for that event with the Window menu. The event handler
gets the open window list by calling owner.getOpenWindowList()
and uses it to rebuild the menu before it appears on the screen.
Here is the code, from the BrowserWindow
class.

private void populateWindowMenu() {
    ArrayList<BrowserWindow> windows = owner.getOpenWindowList();
    while (windowMenu.getItems().size() > 4) {
           // The menu contains 4 permanent items.  Remove the other
           // items, which correspond to open windows and are left 
           // over from the previous time the menu was shown.
        windowMenu.getItems().remove(windowMenu.getItems().size() - 1);
    }
    if (windows.size() > 1) {
           // Add a "Close All" command only if this is not the only window. 
        MenuItem item = new MenuItem("Close All and Exit");
        item.setOnAction( e -> Platform.exit() );
        windowMenu.getItems().add(item);
        windowMenu.getItems().add( new SeparatorMenuItem() );
    }
    for (BrowserWindow window : windows) {
        String title = window.getTitle(); // Menu item text is the window title.
        if (title.length() > 60) {
                // Let's not use absurdly long menu item texts.
            title = title.substring(0,57) + ". . .";
        }
        MenuItem item = new MenuItem(title);
        final BrowserWindow win = window; // (for use in a lambda expression)
            // The event handler for this menu item will bring the corresponding
            // window to the front by calling its requestFocus() method.
        item.setOnAction( e -> win.requestFocus() );
        windowMenu.getItems().add(item);
        if (window == this) {
               // Since this window is already at the front, the item
               // corresponding to this window is disabled.
            item.setDisable(true);
        }
    }
}

And that just about covers things. As you can see, it’s not very difficult to
manage a multi-window application. And it is wonderfully easy to write a reasonably
functional web browser in JavaFX. This has been a good example of building on existing
classes. And we’ve also seen some nice new examples of working with events.
With that, we have almost reached the end of this textbook. The
final section will cover a few odds-and-ends of GUI programming.

License

ITP 220 Advanced Java Copyright © by Amanda Shelton. All Rights Reserved.