> Java > apps > fontviewer > explore > explore
> Java > apps > fontviewer > explore > explore
by ZweiBieren   Java coffee cup logo
 
red x in circle
Every time you click on a feature or subset, this tab scrolls and refreshes. If you drag it to be a separate window, it won't hide the SourceExplorer tab.

Code Notes

See help file for how to use the source explorer window.

Print a list of all fonts

LIST

Simple list printing code

Code essential to being an executable program

backbone

package physpics.com.fontviewer;       
import javax.swing.*;
public class FontViewer extends JPanel {
    . . . 
    public static void main(String[] args) {
        . . .
    }
}

Every Java program source file is the declaration of a public class. The class name is the same as the name of the file. The package statement disambiguates the class name by slotting it among all the other files from the same vendor; for FontViewer, the vendor physpics.com asserts that it will only generate one family of source code in the "fontviewer" group and that FontViewer is in that group. As a consequence of the class name and package declaration, we can know that theFontViewer source is in a file

.../com/physpics/fontviewer/FontViewer.java

FontViewer will be a rectangle on a screen. When I write screen rectangle objects, I need to decide what data is part of the object and what is part of the application. It helps if I imagine that my rectangle object may have more than one instance within an application. As such, I have written FontViewer as a subclass of JPanel, the Java Swing object that is a rectangle on screen. To support this, I needed to import at least javax.swing.JPanel. The Swing methods satisfy my two criteria for an asterisk: they are easily distinguishable -- all begin with "J" -- and the code will use many of them. So I wrote

import javax.swing.* 

where the asterisk imports all Swing objects.

A Java main program has a public void method main().When I write a class intended as a screen rectangle, I like to test it standalone. So I write a main() method in its class. This is okay; a finished program can have main methods in any or all of its classes; the proper one must be noted at application run-time. (Either on the command line or in the jar file's META-INF/MANIFEST.MF .) For FontViewer, this file is also the application, so its main() executes for both programmer test and user run-time.

Java also supports applets, programs that embed in web pages. It would seem appropriate that one program file could be executed as either a main program or an applet, but this is awkward. The main class of an applet must be a public object that subclasses the Applet class. However, the design of making FontViewer a screen rectangle meshes nicely with Applet technolgy. The Applet object for FontViewer need only create a FontViewer object and display it in the web page.

 

Java tutorial: A Closer Look at the "Hello World!" Application
See the sections on the class declaraion and the main method.

Java tutorial: Lesson: Packages
Read the several pages of the lesson.

Outer level comments - not essential for execution, but useful for understanding the program

comments

The "comments" feature tags a few comments. By marking these comments as a feature, they can be hidden if you just want to scroll through the code. Originally I imagined large initial comments, but they didn't appear, so this feature is small. There are many comments, but they are entwined among the declarations and tagged according to the feature supported by the declared variable.

General remarks on comments

Java comments extend from /* to */ or from // to the end of the line. Comments are important in keeping to the goal of writing a program so it can be revised. Few programs of any lasting value are ever truly finished. Even now I can think of several features that "ought" to be added to FontViewer. Indeed, when I began it simply listed all the fonts. Without comments, the effort to understand the existing code can be greater than that to write it in the first place. (This explains why there are so many pieces of code in the world that do pretty much the same as others.)

One of the early comments describes the screen layout of the application using a two dimensional array of characters

S C TTT
LLLLLL!
LLLLLL!
LLLLLL!

Where letters differ, they denote different rectangles of the image. Exclamation marks indicate a scroll bar. I've found that diagrams like this help me describe all sorts of screen layouts more readbly than text or method calls. And they lead to simple layouts with BoxLayout and Box.

In my photo-tagging tool, the screen has an image in the upper left, with a map below it; to the far right are the full tree of defined tags and a list of the tags assigned to the current image. In between are two rectangles of special purpose tags. The diagram looks like this:

IIIIII XXXX TTT
IIIIII XXXX TTT
IIIIII XXXX TTT
IIIIII XXXX TTT
IIIIII XXXX TTT
MMMMMMM YY AAAA
MMMMMMM YY AAAA
MMMMMMM YY AAAA

You can decide for yourself if the diagram adds anything to the textual description. To me the diagram suggests a simple implementation with two horizontal boxes stacked in a vertical box.

Java is unique in defining a documentation language within special '/**' or JavaDoc comments. Then the Application Programmer Interface (API) is described within the program itself. Placed thus, the description has a better chance of getting updated as the program changes. (Not necessarily a good chance, but at least a better one.)

A JavaDoc comment is best thought of as HTML text with special markers for program documentation. For instance, the diagrams above are between HTML tags <pre> and </pre> to preserve the whitespace as written. HTML can also mark headings, lists, and font changes. Too much HTML, however, detracts from the readability of the comment in an ordinary program editor, so I tend to skip it.

Developers writing libraries need to take care with describing their API. See for instance How to Write Doc Comments for the Javadoc Tool. For my own application programs, I use little more than @param, @return, and @throws. These appear at the end of the JavaDoc comment for a method or constructor. Each is at the start of a line possibly following white space with at most one asterisk in it. The contents of each extends to the next or to the end of the comment. To write these right (:-), read the sections starting with the one on @param. Another useful document is javadoc - The Java API Documentation Generator. (My code also annotates override methods as @Override because NetBeans otherwise marks the line as imperfect.)

Java tutorial: A Closer Look at the "Hello World!" Application
See the section on "Source Code Comments."

The list of all font names available to Java

fontlist

If we are to list all fonts, we better get that list from somewhere. The "fontlist" feature gets the list of all fonts available to Java:

static final Font[] fontList;  //  all fonts known to Java 
static {
    GraphicsEnvironment gEnv =
        GraphicsEnvironment.getLocalGraphicsEnvironment(); 
    fontList = gEnv.getAllFonts();
}  

Variable fontList is "static". It will be computed only once and shared among all instances of FontViewer, if ever there is more than one.

There is a design choice here. There are two methods for getting a list of fonts. One method, getAvailableFontFamilyNames(), gets the names of all font families, Arial, Microsoft Sans Serif, ... . On my system it produces a list of 434 fonts. (Your system will have fewer if you have not installed cygwin.). The other method, getAllFonts(), returns actual Font objects, one for each installed font. On my system there are 741 such fonts. When a family has multiple Fonts, the extras are for Bold, Italic, and Bold Italic versions. When an application or web page asks for an italic version of a font, the system checks first whether there is an Italic member of the font's family; if so, it is used. Otherwise an algorithm is run to generate an italic version by titling the characters of the plain version. In the case of Arial, there are actually five families: Arial, Arial Narrow, Arial Black, Arial Rounded MT Bold, and Arial Unicode MS. Of these the first two have families with four members and the others are singleton families. Where the singleton has an implied slant or weight, sayt with "Oblique" or "Black" in the name, changing styles in FontViewer will show these fonts as distorted by the algorithm; the result is often not pretty.

In a more general architecture each FontViewer object would be able to support a different set of fonts. Such generality would add a bit of code complexity and contradict the application's goal of viewing all available fonts.

Java tutorial: Physical and Logical Fonts

Non-GUI version - display the font names in the output

offline

Within FontViewer declarations:

static public void printFontList(PrintStream ps)
    for (Font f : FontViewer.fontList)
        ps.println(f.getFontName());
}
Called from main() with:
public static void main(String[] args) { 
    FontViewer.printFontList(System.out);
} 

printFontList() is made general by accepting a PrintStream as an argument. A client application can then supply any destination. Here the for-statement iterates over all Fonts in fontList. The qualifier "FontViewer." is necessary because printFontList() is static; there is no this and no FontViewer object is required to call it.

The for-statement in printFontList() is called a "foreach" statement; the word "for" is pronounced "for each" and the colon is pronounced "in". Be careful writing foreach statements, the syntax varies considerably from language to language.

You may wonder why the contents of printFontList() are not simply placed inside main(). For a quick one-off program, they should be. I wrote the code as it is for two contradictory reasons. First, this way the printing code can remain in the program code augmenting the interactive version (whether or not printFontList() is actually called.) Second the presence of printFontList()in the code can be interpreted by the FontList applet to mean that the text list should be produced instead of the applet. Sadly, to exploit the second reason, printFontList() must be absent from interactive versions.

Java tutorial: The for Statement

Open a window showing a table listing all fonts

WINDOW

sample window for subset WINDOW

no description

Table column that shows the font name for each row

fonts

At the very least, a font viewing system ought to list the fonts. This feature adds the column of font names to the FontsTable. To do so, it declares array array of Strings to hold the names, copies each name to the array, and adds the array as a column of the table.

int fontColIndex = -1;
. . .
final String[] FontColumn
        = new String[fontList.length];
for (int inx = 0; inx < fontList.length; inx++) {
    String fontname = fontList[inx].getFontName();
    FontColumn[inx] = fontname;	
}
. . .
fontColIndex = appendColumn("Font Name", FontColumn, 
    75, 200, 300, getDefaultRenderer(String.class), null); 

The width limits of 75, 200, and 300 allow the column to vary somewhat in width as the window width is changed. In practice, the preference for 200 pixels means that the font column will stay wide until the window width squeezes all the columns.

The last two arguments to appendColumn() are the cell renderer and the cell editor. JTable does not itself paint or edit the contents of each cell. Instead it overlays that cell with a renderer or editor. It adapts the renderer to the contents of the cell, gives the renderer the screen size and position of the cell, and has the renderer do its thing. JTable provides default renderers for Object, Number, and Boolean, but the one for Object merely calls toString on the Object and displays the resulting String in a JLabel. When the code above asks for the default renderer for String.class, JTable provides the one for Object, a superclass of String. Then the String's toString() method returns the String itself and that gets painted in the cell.

Editing is similar, but the editor supplied above for the font names column is null; no editing is allowed.

Java tutorial: Setting and Changing Column Widths

Fundamental code to display a window; builds on the backbone code

listwindow

Time to show something on the screen! The "window" feature opens a window, though an empty one. First, the FontViewer constructor puts a border outside the window and establishes a layout manager:

public FontViewer() {
    setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
    setLayout(new BorderLayout(6, 6));    // gaps of size 6
}

None of this is strictly necessary, a bare JPanel can be displayed. This code lays the groundwork for later additions; each of the elements of the window is added. Although both lines in the constructor refer to Borders, these are unrelated. The setBorder() call leaves an empty edge of six pixels all around the edge of the window. The setLayout() prepares the window to have nine sections, like a tic-tac-toe board, where the bulk of the appication is nominally placed in the center section. FontViewer uses only the center section and the one above it. (Originallly the sample text was in the bottom edge section. When I moved it to the top, I could have switched to a JSplitPane. Better would have been Box.createVerticalBox() from the beginning.)

My scheme for screen rectangle objects is to pretend that there may be multiple ones in an application. In later features, the FontViewer constructor will be revised to take an argument, the font categories database. This same database should be used for all FontViewers, so it is maintained and provided by the application. For testing (and for the FontViewer application) , the main() method needs to be a rudimentary application. Fortunately, little code is needed.

final FontViewer subject = new FontViewer();
final JFrame frame = new JFrame(" Font Viewer");
frame.setContentPane(subject);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
javax.swing.SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        frame.setPreferredSize(new Dimension(725, 800));
        frame.pack();
        frame.setVisible(true);
    }
});

The first line creates an instance of the FontViewer object, the second creates a screen window, and the third places the FontViewer object into the window. Within the run() method the window's size is set, its window-related fields are initialized, and the window is made visible. (Note the initial space in the argument to the JFrame() constructor. The argument is the window title. The space separates the title from the icon that will be added later under feature "laf".)

After calling invokeLater(), the program terminates! Its process ends. Why does the window stay open? Java programs written with java.awt--and this includes JPanel and the other Swing objects--have a separate "event dispatch thread" (EDT) where window related processing occurs. Runnables passed to invokelater() are run on the EDT. Mouse events and keystrokes are fielded by the EDT. Most of the listener objects are called by the EDT. The EDT runs only one task at a time, so you are guaranteed that if one listener is active, none other will be. Tasks passed to invokeLater() are processed between calls to listeners. So when does the EDT terminate? That is the purpose of the setDefaultCloseOperation(). When the window is closed, it is disposed. When the last window is closed, the process ends. (The value EXIT_ON_CLOSE forces an immediate exit. If multiple windows are open, all of them will die as well. Seldom is this what you really want.)

Java tutorial: Using Top-level Containers
The main Java tutorial on creating Graphical User Interfaces (GUIs) is Learing Swing with the NetBeans IDE. It is enthusiastic about the Netbeans GUI design mode. Much as I love NetBeans for coding, the design mode is poor. I am not a fan. The layout software is buggy and the resulting programs are difficult to deal with. I believe you will save time by using Frames and JPanels as I have done in FontViewer. Nonetheless, I often begin with the NetBeans GUI designer; it lets me see what the user will see on the screen and helps me imagine what new features are needed or how the interface can be simplified. After an initial round of development, I can throw away the NetBeans GUI and switch to direct coding of the GUI components. I recommend the Box object as a simple tool for many screen layout tasks.

 

TODO

How to choose what should be in the object and what in the main(): assume the object will be on the screen in several different places OR the object will appear with different sets of fonts.

wrapwithJFrame
insertMenuItems
finalsave
Implement scrolling of the table; also provides column headers

scroll

With several hundred fonts on most systems, the FontsTable would far exceed the screen; hence scrolling.   Java Swing makes adding scrolling almost painless. To make a view V scrollable, one creates a JScrollPane and puts V into the scrollpane's "viewport". Beside the viewport, the scrollpane displays vertical/horizontal scrollbars. Users have learned how to negotiate scrollbars into scrolling the view.

{Curiously, scrollbars may be flipflopping again on an old controversy. Today, when one drags down on a scrollbar elevator the text moves up. The scrollbar background is an analog of the document. This was not always obvious; early on some developers argued that moving the elevator down should move the text down, making the scrollbar background the analog of the window.  With the advent of dragging the text to implement scrolling on cell phones and tablets, the window analog version has been adopted on some platforms. May confusion merrily reign!}

As this document advances to later features, the scrolling code gets elaborate. A perfectly workable version is the first. The FontView global declarations declare the scrollpane as JScrollPane scrollTable; Then the constructor creates the scrollpane and sets the FontsTable as the contents of the scrollpane's veiwport. Finally, where mainTable was added to BorderLayout.CENTER the scrollpane is added instead.

scrollTable = new JScrollPane( 
    ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
    ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 
. . .
scrollTable.setViewportView(mainTable);
. . .
add(scrollTable, BorderLayout.CENTER); 

Generally, the code of each Component should be independent of the code of others. Although Scrollbars inevitably violate this principle. It is to the credit of the Swing developers that the interdependencies are reasonable. One dependency is that tables have no headers unless they are within a scrollpane. Done this way, the scrollpane can implement the headers so they do not scroll; they are attached above the viewport instead of scrolled as part of the JTable. Another dependency is that every Component has the methods setAutoScrolls() and scrollRectToVisible(). If autoscrolls are set true, scrolling happens automatically as the focus moves between table cells. The more elaborate scrolling features in FontViewer require turning off autoscrolls for certain operations. The Component's client can drive scrolling by calling scrollRectToVisible (which does nothing if the component is not in a scrollpane). ScrollRectToVisible does not guarantee the the rectangle will occupy anyparticular part of the visible image. For that clients can call the ViewPort's setViewPosition() method. Indeed, this is exactly the method that Component calls to implement scrollRectToVisible.

Java tutorial: How to Use ScrollPanes

 
Table of fonts, categories, and samples

table

Although the main table of fonts fills most of the FontViewer window, the FontViewer object simply creates a FontsTable object and adds it as a sub-component:

FontsTable mainTable;
. . .
mainTable = new FontsTable();
. . .
add(mainTable, BorderLayout.CENTER);

The code for FontsTable is far more extensive, occupying about half the source code. in outline it is this:

private class FontsTable
            extends JTable { 
    DefaultTableModel tableData = new DefaultTableModel() {
        // TableModel method
    };
    // FontsTable constructor
    // appendColumn() 
    // other methods
    // classes for renderers and listeners
}

A JTable is a view of some data; the data itself comes from some object with the TableModel interface. For most tables, the simple approach is to create an instance of DefaultTableModel, a standard class that implements TableModel. DefaultTableModel was just about right for FontViewer, but I overrode the isCellEditable method:

... TableModel method:
public boolean isCellEditable(int row, int col) {
    return false;
}

JTable offers a half dozen routes to defining which columns are editable. After learning them all and finding out where the code first looks, I decided the simplest and most direct route is to override isCellEditable(). As features below add editable columns to the table, a line for each is added. It tests the col value and returns true if that column is editable.

The FontsTable constructor does all the work of setting up the columns and inserting their data. After some initial parameter setting, the constructor has three sections where eack column feature must add an item: declaration, data assignment, and insertion of the column into the model:

... FontsTable constructor
public FontsTable() {
    setFillsViewportHeight(true); 
    setCellSelectionEnabled(true); 
    setGridColor(new Color(200, 240, 255)); 
    getTableHeader().setBackground(normalHeaderBkgd); 
    setRowHeight(INITIALFONTSIZE + ADDFORROWHEIGHT); 
    setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
    // declare the object arrays to be the columns
    for (int inx = 0; inx < fontList.length; inx++) {
        // add data from fontList to the column arrays
    } 
    setAutoCreateColumnsFromModel(false); 
    setModel(tableData);
    // appendColumn for each column
}

Most of the defaults for JTable were suitable for FontsTable, but I did have to set several, as shown. The appropriate row height turns out to be a constant number of pixels higher than the fontsize; this number, 8, is the value of ADDFORROWHEIGHT. The rightmost column is the sample text; it is the most varied item and is appropriate for resize. In practice, resizing also adjusts some of the other columns. But anyway, the user can drag column boundaries.

Adding columns to the TableModel and the view requires half a dozen assignment statements each. I find it more convenient to write and clearer to read if I introduce a method that just does all those assigments. The values to assign are the arguments to the method and the compiler checks that I have all the arguments so all assignments will be done. (One downside is that I have to assign every parameter, even when the defaults would suffice. The column add method is:

final int appendColumn(
    String name,                     // column header
    Object[]data,                    // data for the column (usually Strings)
    int minW, int prefW, int maxW,   // column width constraints
    TableCellRenderer render,        // object to render each cell
    TableCellEditor edit) {          // object to edit cell
    int index = tableData.getColumnCount(); 
    tableData.addColumn(name, data); 
    TableColumn col = new TableColumn(index, prefW, render, edit); 
    col.setMinWidth(minW); 
    col.setMaxWidth(maxW); 
    addColumn(col); 
    return index; 
} 

The index returned by appendColumn identifies the column. These will change as the number of features included varies. The variables xxxColIndex are assigned these values and runtime column tests are against these variables. A line in isCellEditable() is like this:

if (col == catColIndex) return true;

Note that the code first adds a new column to the model with tableData.addColumn() and then adds a column to the JTable with plain addColumn. Both are necessary because setAutoCreateColumnsFromModel() has been set false. I set it false because the defaults are not quite what I wanted. The JTable view column is created with the TableColumn constructor listing the the width, renderer, and editor. The minimum and maximum width are also set. More about renderers is with the "fonts" feature.

The column widths in all calls to appendColumn() are given as integer constants. This is highly objectionable stylistically, and will fail on a higher resolution display device. The right way to do this is by computing widths from font metrics. I finally decided to simplify the code by writing constants.

Before implementing appendColumn, I explored the several ways to specify the renderer: override JTable.getCellRenderer(), override TableColumn.getCellRenderer(), call TableColumn.setCellRenderer(), override TableModel.getColumnClass(), call TableModel.setColumnClass(), call JTable.setColumnClass(), call JTable.setDefaultRenderer(). I finally settled on using a parameter to the TableColumn constructor because it conveniently combined several options and because I had specialized renderers for most columns.

Another situation where there were many alternatives had an opposite problem: most approaches failed. The goal was to get some spacing between the column borders and the ends of the string in the column. The one I had the most hope for was JTable.getColumnModel().setColumnMargin(). Its failure was in setting margins that were about half what was specified. What finally worked was to specify a border around the cell in the cell renderer.  Two borders were used, with a blue border for a cell with focus. These are declared among the FontViewer global variables, which is where I put constants that affect appearance:

static final javax.swing.border.Border cellBorder 
    = BorderFactory.createEmptyBorder(0, 3, 0, 0); 
static final javax.swing.border.Border borderBlue 
    = BorderFactory.createCompoundBorder( 
        BorderFactory.createLineBorder(Color.BLUE), 
        BorderFactory.createEmptyBorder(0, 2, 0, 0)); 

Java tutorial: How to Use Tables

TODO

there are no table headers if scrolling is not enabled

viewer.initialFocus in method main

origanlly the code called veiwer.mainTable.requestFocusInWindow; but it is objectionable (and NetBeans raised the objection) to call a method on an object within another. Hence I introduced a method that main can call to get the job done. Indeed that method is now an entry point where a FontViewer object can perform additional tasks when the client is through setting up.

appendColumn() is a helper method designed to avoid repetitive code

setautoresizemode is not NEEDED because we have set size constraints

iscelleditable is required even though null is passed for editor


if do scrollbar w/ mouse, the elevator moves fine
but if next click page up or down,
the scroll reverts to the latest selection
and scrolls via keystroke </p>
<p>set the selection
could cause selection to happen from mouse scrolls
OR
ignore selection when doing key scrolls
--- </p>
<p>PGUP moves seleted line to just off the bottom
then selects the top line
PGDN moves selected line to top
then selects the line at the bottom
NO
revise to
PGUP - select line third from top, then proceed
PGDN - select line third from bottom, then proceed


Add a column to the table showing a text sample in that row's font; add a widget at the top to choose the displayed sample text

SAMPLES

sample window for subset SAMPLES

no description

An editable TextField to edit the sample text shown in the Samples column of the table

edittext

It is not enough to show the names of all fonts; each can be best understood by viewing a sample in that font. When a user has a specific string to render, it can be entered in the sampletext widget. Initially, the text to display should reveal as many text features as possible. For FontViewer the initial sample text shows all lower-case alphabetics, two ligatures, and many common digrams and trigrams:

static final String defaultText
    = "Fred fixed the zoo flight's problems "
        + "by quickly waiving his objections.";

One source for letter and digram frequencies is the University of Bristol.

Subsequent global declarations in FontViewer are for a variable to retain the sample text editor and the current text value:

DemoText sampleEditor;
String currentSample = defaultSample;

At this stage of development, a JTextField suffices instead of DemoText; defining the DemoText as a subclass prepares the way for later developments. The currentSample variable contains the value currently displayed in the sample column; it will differ from the sampleEditor value while the user is changing the text in sampleEditor.

The FontViewer constructor creates the text viewing widget:

sampleEditor = new DemoText(defaultText);
. . . 
top.add(sampleEditor);

The DemoText object sets a font and margins:

class DemoText extends JTextField {
    public DemoText(String starterText) {
        super(starterText);
        setFont(labelFont);
        setMargin(new Insets(3, 8, 3, 0));
    }
}

Exercise: DemoText serves to separate the java code for editing the sample text from the rest of the FontViewer constructor. Show how to write the above code as a JTextField initialized directy within the FontViewer constructor.

Java tutorial: How to Use Text Fields

Table column showing the sample text in this row's font and the current size and style

seetext

The rightmost column of the table is the sample text shown in the named font. Since this is just a string, it is "obvious" that it should be as easy as showing the name of the font. Nope. Lots more is needed, mostly when the sample text can change.

The first issue is what object to store as the "value" of the cell. The string displayed is the same for all rows, so there is no point in having it as the value. What does change is the font, so that is what I made the value; for an interesting reason it had to be a subclass of Font. The renderer, FTFontRenderer, renders fonts by drawing the sample text in that font. Here is the basic sample text column machinery:

int sampleColIndex = -1;
. . .
SampleFont[] sampleColumn = new SampleFont[fontList.length]; 
for (int inx = 0; inx < fontList.length; inx++) {
    String fontname = fontList[inx].getFontName();
    sampleColumn[inx] = new SampleFont(fontList[inx]);
}
. . .
sampleColIndex = appendColumn("Sample written in named font", 
    sampleColumn, 75, 300, 10000, 
    new FTFontRenderer(), null); 
    . . .
    mainTable.refontTheSamples();

The last line initializes all the SampleFont objects to the initial values of font style and size.

If the value in the sample column was a Font object itself, copying from the table would not produce the sample text, but would return a value from Font.toString(). All JComponents implement toString() with a default that gives the object's name and parameter string. Nothing like what a user might expect from selecting and copying an instance of the sample text. For this reason, a subclass of Font is needed that provides its own toString() method:

class SampleFont extends Font { 
    public SampleFont(Font f) { super(f); } 
    public String toString() { return sampleEditor.getText(); } 
} 

It is always true that the first thing a constuctor does is to invoke the constructor for the supeerclass to initialize the superclass local fields. If no arguments need to be passed for the superclass constructor, the subclass constructor need do nothing special. To pass arguments, as here, the superclass constructor is called with the syntax super( <superclass constructor arguments> ).  Font has indeed a constructor which takes a font as its argument; the newly constructed Font is a copy of the argument Font. SampleFont has no local variables, so it does no initialization of its own.

The renderer for SampleFont table cells is indeed similar to FTStringRenderer. It just gets the string from the sample text instead of from the cell's own value. That cell value is instead inserted as the font for the cell:

class FTFontRenderer extends DefaultTableCellRenderer {
   public Component getTableCellRendererComponent( 
            JTable table, 
            Object value, // the SampleFont object
            boolean isSelected, boolean hasFocus, 
            int row, int column) { 
        setText(currentSample); 
        setFont((Font)value); 
        setBorder(hasFocus ? borderBlue : cellBorder); 
        setForeground(hasFocus ? Color.BLUE : Color.BLACK); 
        return this; 
    } 
}

Other than setting the text and the font, this class is the same as FTStringRenderer.

Java tutorial: Inheritance (subclasses)
Java tutorial: Overriding and Hiding Methods

The samples code is about the same as that for a column of strings; buit that is not enough. For the sample column we must also react as the user edits the sample text via the top widgets: font size, font style, and sample text. To react to changes in these widgets, we add a listener to each just after creating it:

sizes.addChangeListener( 
    new javax.swing.event.ChangeListener() { 
        public void stateChanged(javax.swing.event.ChangeEvent e) { 
            mainTable.refontTheSamples(); 
        } 
    }
); 
styles.addItemListener(new ItemListener() { 
    public void itemStateChanged(ItemEvent e) { 
        mainTable.refontTheSamples(); 
    } 
});
sampleEditor.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) { 
        mainTable.reviseSamples(); 
    } 
}); 

To react to changes the code calls refontTheSamples() and reviseSamples(). These are described in just a bit.

The sampleText is a DemoText object, which is a subclass of JTextField. As such, it fires ActionPerformed when the ENTER key is typed.  In order to keep the currentSample value up-to-date and to avoid firing unnecessary ActionPerformed events, I overrode the method that JTextField calls to begin an ActionPerformed:event:

public void fireActionPerformed() {
    String nowText = getText();
    if (currentSample.equals(nowText)) return;
    currentSample = nowText;
    super.fireActionPerformed(); 
}

So the ActionPerformed event occurs only when the sample text has actually changed.

For complete responsiveness, I decided that any changes to the text should also be reflected to the sample column when focus leaves the DemoText. This requires adding a FocusListener inside the DemoText constructor; it also calls fireActionPerformed:

public DemoText(String starterText) {
    . . .
    addFocusListener(new FocusAdapter() {
        public void focusLost(FocusEvent e) {
                fireActionPerformed();
        }
    });
}

When the size or style changes, refontTheSamples() is called to create new fonts for the samples column:

private void refontTheSamples() { 
    int fontsize = sizeModel.getNumber().intValue();
    int styleInt = styleValues[styles.getSelectedIndex()]; 
    for (int inx = 0; inx < fontList.length; inx++) { 
        Font newf = new SampleFont(fontList[inx] 
                .deriveFont(styleInt, (float)fontsize)); 
        tableData.setValueAt(newf, inx, sampleColIndex); 
    } 
    repaint(); // re-render all (to get new samples displayed) 
} // end refontTheSamples 

When the sample text has changed, trigger a repaint of all sample cells. Painting will use the currentnSample value.

private void reviseSamples() { 
    for (int inx = 0; inx < fontList.length; inx++) 
        tableData.fireTableCellUpdated(inx, sampleColIndex); 
} 

It is important to understand that there is n magic in  event handling. The work of super.fireActionPerformed is done with a loop over all registered listeners. The loop simply calls the actionPerformed() methodo in each listener. In the code above each listener is a unique object dedicated to listening for events from one source.  Before the introductino of anonymous objects, it was more common to just declare the main object, in this case FontViewer, as implementing ActionListener.Then among the object's methods there would have to be actionPerformed(). That maim object would then itself be registered as a listener. If action events could come from multiple sources, the actionPerformed() method could test the Event's .source field to determine what object initiated the call. (Later on, FontViewer will be seen to implement actionPeerformed() to handle menu selection events.

Java tutorial: How to Write an Action Listener

The top section of the screen image, holds the sizer and styler fields; uses a layout manager other than than main one

top

The top feature creates the top portion of the window, containing the widgets for setting the sample text: size, style, and the the text itself. (The actual widgets are created by the next three features.)

Box top = Box.createHorizontalBox();
  top.add(sizes);
  top.add(Box.createHorizontalStrut(12));
  top.add(styles);
  top.add(Box.createHorizontalStrut(12));
  top.add(sampleEditor);
  top.add(Box.createHorizontalStrut(12));
  add(top, BorderLayout.NORTH);

The first line creates a layout box; a simple container that places its contents one after the other. The middle lines add the widgets, surrounding each with horizontal struts to space between them. The last line inserts the completed Box as the component at the top of the FontViewer; that is, at the top of the JPanel of which FontViewer is a subclass. The horizontal glue objects will expand or contract as the Box's width is changed. If a widgets' minimum width is less than its maximum, some of the excess width will also be shared with that widget.

Method add() is common to all Container objects like JFrame and Box. The Component argument is inserted at the end of the Container's list of childerLayout include NORTH, SOUTH, EAST, WEST, and CENTER.

Java tutorial: How to Use BoxLayout

Add top widgets to choose size and style of the text samples; add a checkbox column; add sorting to bring the checked rows to the top

USEFUL

sample window for subset USEFUL

no description

Adds a column of checkboxes; sorting on it brings candidates together

checkboxes

Originally I introduced the "category" column for the user to make notes about fonts for a task. That column evolved into more permanent category information, raising once again the need for a way to flag candidate fonts for a particular pupose.. My solution was to invent a simple check-off column; one click adds the font and sorting on the column brings all candidates together. This became the ""Ck" column. JTable has direct support for columns of checkmarks, so the column was easy to implement.

The code is in the FontTable globals and constructor:

int chkColIndex = -1;
. . .
Boolean[] chkColumn = new Boolean[fontList.length];
for (int inx = 0; inx < fontList.length; inx++) {
    chkColumn[inx] = new Boolean(false); 
}
chkColIndex = appendColumn("Ck", chkColumn, 
        30, 30, 30, getDefaultRenderer(Boolean.class), 
        getDefaultEditor(Boolean.class)); 

The built-in default renderers for Booleans display a JCheckBox and toggle it for a mouse click (or for the space bar when the checkbox has the focus).

The checkbox column is editable as indicated in isCellEditable():

if (col == chkColIndex) return true; 

Java tutorial: JTable: Editors and Renderers , How to Use Checkboxes

A JSpinner to choose the size for the font sample text

picksize

The"picksize" feature adds a JSpinner to the top row for choosing the size of text to display. Among the declarations is INITIALFONTSIZE so the spinner and the initial display can both have the same size. The sizeModel describes the range of values the JSpinner can choose among; it is passed to the contructor that creates sizes, the JSpinner object.

final static int INITIALFONTSIZE = 12;  // fontsize for
  samples SpinnerNumberModel sizeModel // font sizes range
      = new SpinnerNumberModel(INITIALFONTSIZE, 6, 40, 4);
JSpinner sizes;     // will range over sizeModel  

In FontViewer, the JSpinner is constructed and given a ChangeListener to respond to changes in the value. As with the styler object, the response is to recreate the displayed samples to show them in the new size.

sizes = new JSpinner(sizeModel);        
sizes.setFont(labelFont);       
sizes.addChangeListener(new javax.swing.event.ChangeListener() {
    public void stateChanged(javax.swing.event.ChangeEvent e) {
        mainTable.refontTheSamples();   
    }       
});
. . .
top.add(sizes);

Within refontTheSamples(), the current size value is retrieved from the sizeModel:

int fontsize = sizeModel.getNumber().intValue();

In an effort to be general, SpinnerNumberModel records and returns its value as a Number object; it supports floating values as well as integers. SpinnerNumberModel.getNumber() returns a Number, so an additional call to intValue() is needed to retrieve the actual value of the spinner.

Although SpinnerNumberModel is a class, SpinnerModel is not; it is an interface. Any class can "implement" an interface; it must implement all the methods defined by the interface. If AnyClass is a nested class, it can be static if it has no need to refer to values in its enclosing class:

public static class AnyClass implements SpinnerModel
  { public void addChangeListener(ChangeListener l) { ... }
    public Object getNextValue() { ... }
    public Object getPreviousValue() { ... }
    public Object getValue() { ... }
    public void removeChangeListener(ChangeListener l) { ... }
    public void setValue(Object value) { ... }
}

Exercise: Implement AnyClass so it is a SpinnerModel that ranges over roman numerals from I to X. Setting an illlegal value should throw IllegalArgumentException. The simplest approach may be to have an array of the ten valid values. Note that ChangeListener is any Object that has a method "void stateChanged(ChangeEvent e)". This method must be called from setValue().

Java tutorial: How to Use Spinners

A JComboBox to choose bold/italic for the font sample text

pickstyle

The pickstyle widget is a JComboBox for choosing among four font "styles": plain, bold, italic, or bold/italic. It displays the current style and offers a drop menu to choose among all four styles. The widget code begins with declarations of the syles and the widget itself:

static final String[] styleNames // for the styler JComboBox
    = {"Plain", "Bold", "Italic", "Bold Italic"};
static final int[] styleValues
    = new int[]{Font.PLAIN, Font.BOLD,
        Font.ITALIC, Font.BOLD | Font.ITALIC};
JComboBox styles = new JComboBox(styleNames);

In the FontViewer constructor, the pickstyle JComboBox is adapted by setting the font it uses to display the style names and by adding a listener. The listener's itemStateChanged method is called whenever the style changes. It calls refontTheSamples to change the displayed sample texts. This is the same method that is called by the listener for the fontsize selector widget.

static final Font labelFont = new Font("Dialog", Font.PLAIN, 14);
styles.setFont(labelFont);
styles.addItemListener(new ItemListener() {
    public void itemStateChanged(ItemEvent e) { 
        mainTable.refontTheSamples();
   } 
}); . . . top.add(styles);  

Within refontTheSamples(), the style value is retrieved with

int styleInt = styleValues[styles.getSelectedIndex()]; 

this value is then used in setting the fonts for the samples on all visible lines.

Here styleValues and styleNames are two "parallel" arrays; values at the same index are related. Ordinarily I prefer to store related values as fields in objects; perhaps StylePair objects, where each StylePair has a name and a value. To do so would require writing a new class and a custom renderer for the JComboBox; parallel arrays are much simpler.

JComboBox's method addItemListener() takes as argument an object of type ItemListener. One could be created by declaring an innerclass within FontViewer:

private void class StylerItemListener implements ItemListener
     { . . .
}

This class would have one method, itemStateChanged(ItemEvent e), as required by the ItemListener interface declaration. The call to addItemListener() would then be

addItemListener(new StyleItemListener());

Instead of declaring a nested class and referring to it by name, the actual code declares the listener as an anonymous class. The body of the class is written within a pair of curly braces following the arguments to the constructor. (When writing listeners, there are seldom arguments to the constructor.) The body of the class is generally one or more method declarations like that of itemStateChanged().

Java tutorial: How to Use Combo Boxes
Java tutorial: Nested Classes
Java tutorial: Creating Anonymous Classes

Java in a Nutshell: 3.12. Anonymous Classes
Includes a good description of restrictions on anonymous classes.

The very simplest code for allowing sorting on columns (Hidden if the "sort" feature is visible)

simplesort

no description

 

 

moving columns triggers sort;
sigh - need sort buttons

Sort the table on the category or fontname column

sort

to do per-column tooltips, the most general method
is to create a cell-renderer and assign it to a header
with jtable.getColumn(columnNumber).setHeaderRenderer(specialRenderer)

lambda

Lambda QuickStart guide, Lambda Best Practices

Add File and Help menus; add distinct tool tips for each column

MENUS

sample window for subset MENUS

no description

Menu item to show information about the application and its authors

about

The "About" menu item is where the development team gets to say whatever they want about the app. Who wrote it. What does it do. What is the current status. How many pizzas fueled the development. For future sanity, exclamation mark (Pay attention)the about box must also contain a version number and the date of the release.

The About item is added to the Help menu with an action created by FontViewer's createAction method:

// HELP->ABOUT FONTVIEWER	
    helpMenu.add(createAction(FontViewer.this,
        "About FontViewer",
        "FontViewer version and info on categories file",
        null,
        ()->{ about(); }
    ));

The about() call reveals the about box via JOptionPane. First it creates the message as a string of HTML code and puts it in a JLabel. Then this is passed to showMessage(). In FontViewer (without the telluser feature) the steps are

 JLabel lblmsg = new JLabel("<html>" + msg + "</html>");
 JOptionPane.showMessageDialog(this, lblmsg);

For the msg value to include the version, that value is fetched from the .jar file. exclamation mark (Pay attention)This avoids having to keep the version number in more than one place:

    String version = getClass().getPackage()
            .getImplementationVersion();

Then the message is constructed with

    String msg = "<a href='"+SITE_URL+"/'>FontViewer</a>"
            +" version " + version
            + "   by ZweiBieren@PhysPics.com";

Additional message lines are added by code from other features.

Altogether, and with the telluser feature, the FontViewer about box looks like this.

The "about" box displayed by FontViewer

A method to create Action objects from multiple arguments

action

In trying to understand the "Action" object, it is best to think of it as the data model for a generalized button. The putValue/getValue methods map names to values so specific button types can ask for values relevant to themselves. The actionPerformed method is invoked when the button is pushed. The enable/disable methods toggle the button between available and grayed out.

Java's "Swing" component set offers numerous button-like objects that tailor themselves via getValue from an Action (often supplied via setAction). Objects like JButton, JCheckBox, JMenuItem, and even keyboard keys. A standard set of putValue names (which are misleadingly refered to as "keys") are defined in the Action object. Here they are with brief notes as to their usage.

NAME String displayed in a menu or on a button with no icon
SMALL_ICON String displayed on a menu item alongside the NAME
LARGE_ICON_KEY String displayed on a button
DISPLAYED_MNEMONIC_INDEX_KEY Index of letter in NAME to be underlined if the button is a menu item
MNEMONIC_KEY Letter whose first instance in NAME is underlined on a menu; it serves as the "mnemonic" to activate its item from the keyboard
SHORT_DESCRIPTION Displays as a tooltip.
ACCELERATOR_KEY Value is a KeyStroke object. When typed, the action will be triggered (like control-S for Save)
ACTION_COMMAND_KEY The "command" string to be passed as the argument to actionPerformed. Also used as the object stored in an InputMap to link to an Action in the ActionMap. (See resettext.)
SELECTED_KEY

If non-null, the value must be a Boolean object. Its value indicates whether the button is "selected", as in a radio- or toggle-button group.

LONG_DESCRIPTION Unused; could be displayed by a contextual help system

The actionPerformed attribute of an Action is not a getable/setable value; it is a method offered by the object. The Action object must be created in such a way that it overrides the actionPerformed method to do whatever is called for by clicking the button. Usually Actions are constructed as subclasss of AbstractAction.

Sometimes an interface designer chooses to frustrate the user by disabling an action like Save. (I say "frustrate" only because I usually only notice disabled operations when I am doing something the designer didn't plan for.) A disabled key is often shown in gray; it is there but cannot be chosen. Action objects support this with the enable/disable methods; disabling the Action disables all Swing components that share it.

See also: Action javadoc and How to Use Actions

One can construct an Action by creating a subclass of AbstractAction and serially adding named values to the map with putValue. The resulting code is bulky and reader unfriendly. It is preferable to exclamation mark (Pay attention)create a helper method that assigns values from parameters. In FontViewer that helper method is createAction. It has these parameters:

main
JComponent whose action and key maps should be revised for accelerator keys
name
Name that should appear in a menu or button. If the name contains an underscore (_), the following character is identified as the mnemonic (usually underlined). The underline itself is deleted.
tooltip
If not null, the string is displayed as the tooltip for the menu item.
accelerator
If non-null, must be a String in the format defined by KeyStroke.getKeyStroke. Examples "ctrl S" (must be upper-case) "alt typed A" "meta ctrl DELETE"
tobedone
An ActionListener typically created with a lambda expression. See sort for lambda expressions.

createAction returns an Action suitable for Swing buttons and keyboard responses. If additional properties must be added, assign createAction's value to a temporary and add properties to the lattter.

Since createAction returns a value, it can be added directly to a menu:

// HELP->USING FONTVIEWER
helpMenu.add(createAction(FontViewer.this,
        "Using FontViewer",
        "Browse the instructions for using FontViewer",
        "F1",
        ()->browseGuide()  ));

 

Put distinct tooltips on all columns

coltooltips

Tooltips appear when the user hovers the mouse over an item. Typically they describe the effect of clicking the item. The tips proovided by this coltooltips feature are for the column headers in the table of fonts. Hovering the mouse over "Font Name" reveals this tooltip:

tooltip for fontname column reads "Sort by name. Click a name to copy it to the cut buffer."

In most cases, Swing components store data and act on it. Tooltips are an unpleasant exception; you are required to override a method to provide the tooltip. Fortunately, the Table tutorial provides exactly the code needed to implement the method. I was able to copy it directly into FontViewer.

In essence the code begins by creating a subclass of JTableHeader that overrides getToolTipText (the actual code uses an anonymous class)

class MyTH extends JTableHeader{
    @Override
    public String getToolTipText(MouseEvent e) {
        . . .
    }
}

Then the JTable object must override the method that JTable calls to get a table header object:

protected JTableHeader createDefaultTableHeader() {
    return new MyTH();
}

Actual fetching of the tooltip maps the screen click to the physical column, that to the model column number and that to the tooltip:

java.awt.Point p = e.getPoint();
int index = columnModel.getColumnIndexAtX(p.x);
int realIndex = columnModel.getColumn(index).getModelIndex();
return columnToolTips[realIndex];

Physical and model column numbers differ after the user moves swaps columns on the screen by dragging the headers.

In the unlikely case that one tooltip will suffice for all columns, the code need only call setToolTipText for the table header object. When FontViewer is built without the cooltooltips feature the table create code makes this call:

table.getTableHeader().setToolTipText("<html>"
      + "To compare fonts, click under Ck & sort.<br>  "
      + "To sort on any column, click its header.</html>");

The result is that all columns have the one tooltip:

one tool tip for all columns: To compare fonts, click on Ck & sort. To sort on any column, click its header.

Note the use of <htlm> to get a newline into the text.

Open the browser viewing the FontViewer help text.

guide

Feature "guide" opens the user's preferred browser to the page describing the use of FontViewer. It is in two parts; first implement the behavior and then create an Action and add it to the menu.

Asking a browser to open is a function of the DeskTop object and may not be available. Other features--fillcut and telluser--add code to this method so it can try to put the guide's URL into the cutbuffer.

public void browseGuide() {	
    Desktop dt;	
    if (Desktop.isDesktopSupported() 
            && (dt=Desktop.getDesktop())	
                .isSupported(Desktop.Action.BROWSE)) {	
        try {	
            dt.browse(new java.net.URI(HELP_URL));	
            return;	
        }
        catch (java.io.IOException | java.net.URISyntaxException ex) {}
    }
}

The Action that is added to the menu calls the code above. Note that the F1 key is set as the window-global accelerator for browsing the guide. This follows the Windows convention.

helpMenu.add(createAction(FontViewer.this, "Using FontViewer",
        "Browse the instructions for using FontViewer",
        "F1", e->browseGuide()));

 

Java Tutorial: Desktop

Have a window-top pull-down menu

menu

In FontViewer code, the "menu" feature is solely responsible for method insertMenuItems which adds menus and menuitems to a MenuBar that is an argument:

public void insertMenuItems(JMenuBar bar) {
        . . . add menus and items to bar
}

 

Filemenu

The filemenu feature adds the File menu; no more. The elements of the menu are added by features catsreadui, catssave, catssaveas, and shut. The code to establish the menu is

JMenu fileMenu = new JMenu("File"  );
bar.add(fileMenu);

Helpmenu

The helpmenu feature does nothing more than add the Help menu to the menubar. Items are added to the menu by the guide and about features.

JMenu helpMenu = new JMenu("Help");
bar.add(helpMenu);

Actual creation of the menubar and installation to the frame is the responsibility of the application. In the FontViewer main method, this is accomplished with

final FontViewer viewer = FontViewer.create();	
JMenuBar menuBar = new JMenuBar();	
viewer.insertMenuItems(menuBar);
SwingUtilities.invokeLater(()->{	
    JFrame frame = viewer.wrapWithJFrame();	
    frame.setJMenuBar(menuBar);	
        . . . 
    frame.setVisible(true);
});	

So why not add the menubar while constructing FontViewer? The intent is to make it possible that FontViewer could be a component within some larger application. The full flowering of this ambition is Java Beans, but that is beyond the scope of this project. Moreover, FontViewer menu items will conflict with those from other applications. Indeed, having Exit as a component menu item would interfere with Exit in the application or other components.

One menu implementation would be to have the application create a dummy menu bar and call insertMenuItems on it. Then the application could pick and choose which FontViewer menu items to install in the actual menu. Or the application could keep track of the input focus and display the FontViewer menu only when FontViewer has the focus.

To keep track of whether the focus is within a FontViewer widget, you can monitor the keyboard focus manager

KeyboardFocusManager focusManager =
    KeyboardFocusManager.getCurrentKeyboardFocusManager();
focusManager.addPropertyChangeListener(
    new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent e) {
            String prop = e.getPropertyName();
            if (("focusOwner".equals(prop)) {
                  Component owner = (Component)(e.getNewValue());
                  if (SwingUtilities.isDescendingFrom(comp, viewer)
                  	{{display the fontviewer menus}}
                  else 
                  	{{decide what menus to display}}
            }
        }
    }
);

To get ALT to work, all menus and items need setMnemonic MAY also have setDisplayedMnemonicIndex

Shutdown when the user closes the window

shut

Simple, non-graphic Java programs end by reaching the end of the executable code or calling System.exit(0). Programs with a graphical user interface routinely reach the end of the executable code, but they continue operating because setVisible(true) has started a separate thread to manage the user interaction.

Instead of focusing on the aplication, let's look at closing an individual window. It can be closed by action of the user, a container application, or the system. The user would click a menu item--say the EXIT item, or type a key. The containing program might dispose() the window. The window manager might act in response to some user window closure operation like clicking a particular icon in the window's title bar. The various facilities for closing windows and noticing closure are somewhat confusingly named. Pay close attention to the dfference between WindowClosing, WindowsClosed.

In what follows, I talk loosely about "windows". But what I am really talking about is JFrames. They are a subclass of Window and have additional methods, especially setDefault.

Some windows are purely display windows; it makes no difference how they are closed. Others retain some state data that needs to be saved before the window closes. This is also the place to allow the user to recant the closure decision. FontViewer offers this dialog if unsaved changes remain:

File save selection widget with options Save, Don't Save, D0 Not Exit
File save dialog with three options for unsaved data

For user-initiated closures, FontViewer ritually executes

if (closePanel())
    frame.dispose(); 

When a window is dispose()d all its screen resources are discarded. It loses its screen space and any attached offscreen buffers. The program can still recreate it by setVisible(true). Disposing a window is graceful. Only when an application's last window is disposed does the application finally exit.

A container must execute the same sequence to close a FontViewer subwindow. In either case the call to dispose() generates a "WindowClosed" event to any WindowListeners on the frame. This is mostly useless; the code has called dispose so it knows it is happening. Nor can this be part of the answer when the closure might need to be cancelled; since dispose is already happening, the window IS going close.

The interesting bits arise when dealiong with system-initiated closure. When one of these begins, a WindowListener on the window receives a "WindowClosing" event. (This has no relation to a "WindowClosed" event. It does not even mean the window is closing.) It means the user has done something that caused the system to try to close the window.  After processing the event the Java runtime does one of four actions as determined by a prior call to the window's setDefaultCloseOperation method. The options are

WindowConstants.DO_NOTHING_ON_CLOSE
Does nothing. The window stays open.
WindowConstants.HIDE_ON_CLOSE
The window disappears, but is still part of the application and may be reinstated by program action.
WindowConstants.DISPOSE_ON_CLOSE
The window is dispose()d.
JFrame.EXIT_ON_CLOSE
Calls System.exit. This is usually the wrong choice.

Of these FontViewer chooses the first, DO_NOTHING. Only thus can it choose whether to actually close the window. Once that choice is made, FontViewer listens for WindowEvents and responds to WindowClosing by executing the window closure ritual noted above.

It is often desirable that the File->Exit menu item and system-initiated window close behave identically. Both can execute the closure ritual code as described above. Just to show off how I can find things by googling, I wrote the Exit menu code so it behaves exactly like the system-close by simulating the system event:

final Container top = getTopLevelAncestor();
if (top instanceof Window)
    TOOLKIT.getSystemEventQueue()
        .postEvent(new WindowEvent((Window) top,
            WindowEvent.WINDOW_CLOSING));

As fun as this may be, it is really a bad idea. It is brittle. If the Java library changes it this code may no longer work.

For completeness I need to mention that one can capture system shut down events and try to save files then. The invocation is

Runtime.getRuntime().addShutdownHook(()->{
    do something durinng shutdown
} 

This choice should be avoided. As the documentation remarks, "Shutdown hooks run at a delicate time in the life cycle of a virtual machine". Among the undesirable possible outcomes are corrupted files and deadlock.

 

Java tutorial: How to Write Window Listeners

Java tutorial: Responding to Window-Closing Events

Add columns for style and category; a user can edit the categories as desired

CATS

sample window for subset CATS

no description

Table column showing a category for each font

categories

This feature implements the "Category" column in the fonts table. User-editable, this column was originally a place for the user to make notes about the different fonts. In using FontViewer, I evolved its interpretation to style information like "sans serif", "symbol", or "oddball". Users may still extend these notes as they see fit.

Screen shot of left side of FontViewer window with the BI and Categories columns

The categories column is created like all others, with appendColumn (see feature table).

TableCellEditor catEditor = table.new FTStringEditor();	
catColIndex = table.appendColumn("Category", table.categoryColumn,	
        25, 75, 150, "Sort by category, with non-blanks at the top",	
        stringComparator,	table.new FTStringRenderer(LABEL_FONT),	
        catEditor);

where the parameters are the column title, its data, width constraints, tooltip, comparator (see sort), cell renderer (see cellrender), and cell editor (see celled). The returned value is the model index for the column. Ordinarily this value should be a static final int and in all-caps. Having it be a variable allows it to have different values depending on which features are being compiled.

The column index appears in several contexts that are part of the categories feature:

  • createFontsTable.addKeyListener - ignore keys typed into categories column
  • FontsTable constructor - create array to hold category strings
  • editCCP.doEnable - enable edit menu items if focus enters category column
  • DefaultTableModel.isCellEditable - respond that category column is editable

To store values in the categories column, a setValueAt method is offered by both JTable and TableModel. exclamation mark (Pay attention)These are not the same. The column number passed to the JTable version is mapped from display-index to model-index. (They differ after the user has moved columns.) When using catColIndex, for instance, it is important to call TableModel.setValueAt(..., ..., catColIndex).

Categories data is loaded and saved by features catsinit, catsinitjar, catsload, and catsmap.

Read initial values for categories column from the default location

catsinit

Column Category displays data loaded from file(s).  This "catsinit" feature tries to read initial data from various sources, using the file location syntax of StringMap.

When an application offers data that can be modified by the user, there is a problem. If the user changes and saves the data, how does the application find the updated file the next time it is loaded. I choose to save it as a file in FontViewer/fontcategories.txt in the user's home directory.

loadInitialCategories reads files in override order; the string that applies for a font is the last one encountered.

  • jardir:fontcategories.txt
  • http://physpics.com/Java/apps/fontviewer/fontcategories.txt
  • resource:/fontcategories.txt
  • userdir:FontViewer/fontcategories.txt
Read current categories from the distribution site

catsinitjar

no description

Load categories data

catsload

StringMap() constructor used to do read()
but that leaves an overridable method in the constructor
(where it might be executed before the object is fully constructed)

Remember what categories are recorded for each font

catsmap

no description

The first two columns show I for Italic and B for Bold

styles

The feature name "styles" refers to font styles. The BI column shows whether the font is plain (blank), bold (B), italic (I), or both (BI):

Screen shot of left side of FontViewer window with the BI and Categories columns
left side of table, "BI" and "Category" columns

 

Additional user options

UI

sample window for subset UI

no description

Display a dialog box with multiple response buttons

askuser

no description

Automatically copy a clicked-on fontname to the cut buffer

autocopy

no description

Edit cells in the categories column

celled

no description

Menu items for cut/copy/paste

ccpaction

no description

Select the entire contents of a cell when opening it for editing

edpresel

no description

Ensure closure of the editing category item if menu actions occur

finishedit

text

getEditorComponent gets a JTextField during shutdown; ClassCastException
public void cancelEditing() { //=celled
CellEditor ed = (CellEditor) getEditorComponent(); //=celled
if (ed != null) //=celled
ed.stopCellEditing(); //=celled
} //=celled
getEditorComponent returns type Component
use getCellEditor instead (why both?!)

there IS a setCellEditor
there is NO setEditorComponent
(the editor Component is a value returned by a method in CellEditor)

 

 

Replace the sample text with the original "The quick brown …"

resettext

The resettext feature reverts the sample text to its default. Although not particularly valuable to the user, this feature is an excellent opportunity to study Java Swing "Action"s and especially how to invoke them with keystrokes. The feature is implemented by creating a single Action and adding it to a menu bar menu, a popup menu, and a keystroke. The menu items look like this

The edit menu with options for cut, copy, paste and reset sample     A popup menu displays as a box with its upper left where the maouse clicked
"Reset" in a pulldown menu   "Reset" in a popup menu, with tooltip

The Action is created with createAction

resetAction = createAction(FontViewer.this, "_Reset sample",
        "Reset the sample text to its original value",	  
        null, // no window-wide accelerator key
        e->setText(DEFAULT_SAMPLE));

When the menu item is clicked the lambda expression will call setText(DEFAULT_SAMPLE).

resetAction is appended to a JMenu with a simple add(). For the popup, the popup must be created and then attached to the JTextField. In this code variable text refers to a newly constructed SampleText:

JPopupMenu resetMenu = new JPopupMenu(); 
resetMenu.add(new JMenuItem(text.resetAction));
text.setComponentPopupMenu(resetMenu);

Keystrokes are delivered to component code in a process called binding. The easiest way to do this is to set a keystroke as the value of ACCELERATOR_KEY in an Action, as is done by setting the fourth argument to createAction. However for instructional purposes let's decide that contol-R should cause a reset of the sample text only when that text has the focus. If bound by ACCELERATOR_KEY, control-R would clear the sample if the input focus were anywhere in the window.

Binding Keys

Binding a keystroke to an action is a two step process. First the key is mapped by an InputMap to an object amd then that object is mapped by an ActionMap to an Action. When the key has made it through this maze, the Action's actionPerformed method is called. Each component has three InputMaps and only one ActionMap. The Input map used by ACCELERATOR_KEY is the one that applies no matter where the focus is in the window (WHEN_IN_FOCUESED_WINDOW). The InputMap we need to restrict control-R to sampleText is the one the applies when the focus is in the component itself (WHEN_FOCUSED). The third InputMap applies when the component has focus itself or has a child component that has the focus (WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).

The KeyStroke to be bound can be made with any of the static methods of KeyStroke. For simplicity, createAction requires the text name of the key and does the KeyStore call itself.

The Object to map from InputMap to ActionMap is typically a String hinting at the effect of the Action. Our code use "reset sample text". Distinct Strings are generally unambiguous unless they have the same text and appear in a single .java file.

Given a SampleText text, we bind it to control-R like this:

KeyStroke ctlR = KeyStroke.getKeyStroke("control pressed R");
String resetCommand = "reset sample text";
text.getInputMap(JComponent.WHEN_FOCUSED).put(ctlR, resetCommand);
text.getActionMap().put(resetCommand, resetAction);

The extra mapping, from KeyStroke to InputMap to ActionMap to Action adds a level of indirection which may be useful in large applications, especially where internationlization may need to remap keystrokes to actions. The InputMap is supposed eventually to also incorporate mouse behaviors like strokes and circles. Input methods are also important in non-alphabet languages where multiple keys may be needed to select a given character.

Java tutorials: How to Use Menus, How to Use Key Bindings, How to Use the Focus Subsystem

note: could enable disable the reset button

Display a message to the user; replaces a few lines of calling JOptionPane

telluser

no description

Numerous tweaks to improve the user experience

LAF

sample window for subset LAF

no description

Tweak a few global look and feel parameters

adjustlaf

At one time, the Java default lookd and feel dealt poorly with label borders. So I set the look and feel to SystemLookAndFeel. The Java version has since been corrected, so there is no need to set the look and feel.

So the following deleted code is no longer in the source. But it does show how to set the look and feel.

    
    //@[ ~ ~FontViewer setSystemLookAndFeel() : Do not use "Metal" LAF
    public static void setSystemLookAndFeel() {                                 //=systemlaf
        // The default UI is "metal" but it has an ugly JSpinner                //=systemlaf
        // So we use the native look and feel..                                 //=systemlaf
        try {                                                                   //=systemlaf
            String className;                                                   //=systemlaf
            className = UIManager.getSystemLookAndFeelClassName();                //=systemlaf
            className = UIManager.getCrossPlatformLookAndFeelClassName();
            className = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
            className = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
            className = "com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel";
            className = "javax.swing.plaf.metal.MetalLookAndFeel";
            className = "javax.swing.plaf.nimbus.NimbusLookAndFeel";
            
            UIManager.setLookAndFeel(className);                                //=systemlaf
        }                                                                       //=systemlaf
        catch (ClassNotFoundException | UnsupportedLookAndFeelException         //=systemlaf
                | IllegalAccessException | InstantiationException ex) {         //=systemlaf
            // unlikely and not vital, ignore the error                         //=systemlaf
        }                                                                       //=systemlaf
    }                                                                           //=systemlaf
//@] ~ ~FontViewer setSystemLookAndFeel() : Do not use "Metal" LAF
    

The call to the above was in method main:

setSystemLookAndFeel();
This sort can resort on the same column as the previous sort;

bettersort

Simplesort has a number of deficiencies which are corrected by bettersort.

* header color change does not really work because the biggest time is in redrwaing

to do per-column tooltips, the most general method
is to create a cell-renderer and assign it to a header
with jtable.getColumn(columnNumber).setHeaderRenderer(specialRenderer)

When the size or style is changed, this feature returns the keyboard focus to it former cell in the table

cellfocus

no description

 

setAutoScrolls is needed to prevent flash - otherwise tries to update before having all the data

 

Render table cells with distinct fonts and borers

cellrender

With the default renderer, the cell in focus is indicated by highlighting the entire row and boxing the clicked cell. For FontViewer I preferred to blue border the cell and show its contents in blue. For this I needed a custom renderer. The renderer also solved the problem of making wider side margins on the cellls. To install my renderer, the appendColumn call needed a different sixth argument:

fontColIndex = appendColumn("Font Name", FontColumn, 
    75, 200, 300, new FTStringRenderer(), null);

Rather than extend JLabel, the renderer extends DefaultTableCellRenderer, which in turn extends JLabel. This way I reap the performance benefits described in the DefaultTableCellRenderer documentation. As must all renderers, this one overrides getTableCellRendererComponent:

class FTStringRenderer extends DefaultTableCellRenderer {
    public Component getTableCellRendererComponent( JTable table, 
            Object value,   // the Font for this sample cell 
            boolean isSelected, boolean hasFocus, int row, int column) { 
        setText((String)value); 
        setFont(labelFont); 
        setBorder(hasFocus ? borderBlue : cellBorder); 
        setForeground(hasFocus ? Color.BLUE : Color.BLACK);
        return this; 
    }
}

When the method returns "this", it is returning the DefaultTableCellRenderer, which is itself a JLabel. It is this JLabel that paints the cell.

Java tutorial: Using Custom Renderers

Code for filling the cut buffer

fillcut

no description

When user types 'B' or 'I', toggle bold or italic column

keystyles

no description

Display the FontViewer logo on the window decoration and in dialog boxes

logoimages

no description

Adjust the height of table rows depending on fontsize

rowheight

no description

When the size or style changes, scroll back to the previous view

rowvis

no description

Draw the corner at the intersection of the header and the scrollbar

scrollcorner

no description

Sort the samples column by the width of the sample in that font

sortbywidth

no description

Display a title for the sizer and styler boxes above the table

titled

FontViewer widget row
Top with clean titles

It is fair to ask whether the top widgets need labels. Aren't they pretty obvious? But I've found that what is obvious to me may not be to others. And, besides, the titles look more "professional". The TitledWidget class saves duplication of code and produces the appearance I wanted, with title above the beginning of the widget and no border other than the line around the widget itself.

See the image at the right.

With TitledWidget, titles are added by wrapping an object creation around the widget, sizes

top.add(new TitledWidget("Font Size", sizes, true));
. . .
top.add(new TitledWidget("Font Style", styles, true));
. . .
top.add(new TitledWidget("Sample Text", sampleEditor, false));

TitledWidget is a JPanel organized with a BorderLayout that combines a JLabel and the widget.

public class TitledWidget extends JPanel {
  public TitledWidget(String t, 
        JComponent widget, boolean fixedWidth) { 
    setLayout(new BorderLayout()); // layout for JPanel 
    JLabel lbl = new JLabel(" "+t); 
    lbl.setForeground(Color.BLUE); 
    add(lbl, BorderLayout.NORTH); 
    add(widget, BorderLayout.CENTER); 
    ... // set width
  }
}

It is a bit of a kludge to appenda space at the start of the label to line it up the way I wanted. The "right" way to do this is to wrap the JLabel in an EmptyBorder with spacing only on the left. But the kludge is less code and works well. An EmptyBorder could also add space between label and widget.

In addition to titles, TitledWidget solves the problem of adjusting for window width changes. Without this code, BoxLayout allocates extra space proportionally to all three widget. The desired effect is to allocate all space to sampleEditor, the third widget. Hence the third argument to the TitledWidget constructor. When this is true the following code applies:

if (fixedWidth) {
  Dimension wpref = widget.getPreferredSize();
  Dimension lpref = lbl.getPreferredSize();
  Dimension pref = new Dimension(
      Math.max(wpref.width, lpref.width),
      wpref.height+lpref.height); 
  setMaximumSize(pref);
}

Note that the sizes widget comes out a bit wide because the label is longer than the preferred size of the widget. But when the window is resized, the first two widgets do not change size.

In the following code, each of the three different TitledBorder calls produced unappealing results

sizes.setBorder(BorderFactory.createTitledBorder("Font Size"));
top.add(sizes);
styles.setBorder(BorderFactory.createTitledBorder(
         BorderFactory.createEmptyBorder(),"Font Style"));
top.add(styles);
TitledBorder sampleBorder = BorderFactory.createTitledBorder(
BorderFactory.createEmptyBorder(),"Sample Text");
sampleBorder.setTitlePosition(TitledBorder.BELOW_TOP);
sampleEditor.setBorder(sampleBorder);
top.add(sampleEditor);
Top of window with three different TitleBorders

These calls produced the image at the right. All of them put the label too far above the widget and surrounded the widget with more white then I wanted.

In the days before the Swing widgets--JPanel and the like--getPreferredSize would fail until the widget was actually installed in a screen window. That was the purpose of addNotify(). A client would override the addNotify() method and do size calculations there. Life is much simpler with Swing because sizes can be compute from information available right from the start.


It is embasrassing to report how complex TitledWidget got before I hit on the approach above. However, there are several queries on various BBoards asking how to do some of the things that I did.

I first went wrong by starting with TitledBorder and then in thinking that a null Layout would be simple. Without worrying about widths, the code looked like this:

public class TitledWidget extends JPanel { 
  JComponent widget; 
  public TitledWidget(String t, JComponent widg) { 
    super(); 
    widget = widg; 
    add(widget); 
    javax.swing.border.TitledBorder widgetBorder 
        = BorderFactory.createTitledBorder( 
                BorderFactory.createEmptyBorder(), t); 
    widgetBorder.setTitlePosition(javax.swing.border.TitledBorder.BELOW_TOP);
    setBorder(widgetBorder); 
    final Insets ins = widgetBorder.getBorderInsets(this);
    setLayout(null); 
    Dimension wpref = widget.getPreferredSize();
    widget.setBounds(ins.left, ins.top, 
            wpref.width, wpref.height); 
    . . . // deal with widths and resizing
  }

Since there is no LayoutManager, the TitledWidget constructor must set the size and location of the widget.. (The title is managed by the border, which occupies the entire JPanel.) The border insets describe the space available inside the border and we want the widget as far to the left and top as possible. The above works well initially, but fails as the window resizes. BoxLayout allocates additional space for al three JPanels, but the JPanels are not re-laid out, so the widgets do not change size. The effect appears as though the HorizontalStruts change size, which they cannot and do not do. The solution was to set the desired sizes of the first two widgets so BoxLayout always gives them the same space.

if ( ! (widget instanceof DemoText)) {
    Dimension size
        = new Dimension(wpref.width+ins.left+ins.right,
                wpref.height+ins.top+ins.bottom);
    setMinimumSize(size);
    setPreferredSize(size);
    setMaximumSize(size);
}

Now the first two wigets and their adjoining struts stayed put when the window size changed. The sample text also stayed its same length as originally allocated; on screen it was truncated or too short when the window's left edge moved. Several queries on the web wondered how to make a text area resize. For this example, the way to do it is to handle componentResized events:

addComponentListener(new ComponentAdapter() { 
    public void componentResized(ComponentEvent e) { 
         widget.setSize(getWidth()-ins.left-ins.right,
         getHeight()-ins.top-ins.bottom); 
    } 
});

Here the getWidth() and getHeight() methods get the dimensions of their own component, the JPanel. The location stays the same, so only the width needs to be changed. (In my first attempt, I omited the "widget." in front of the setSize method. This made the widgets disappear. Can you see why?)

Note: There is no relation between Borders and BorderLayout.
Java tutorial: How to Use Borders
Java tutorial: How to Use BorderLayout

Recognize webstart and disable options that will fail

webstart

no description

Add the user interfacefor saving the category data to a file

IO

My first thought on helping the user choose a font was to let her/m write notes in a separate column next to each font. Soon I began writing font categories into this column, serif, symbol, specialty, and the like. The implementation of the CATegory column is detailed in the subset CATS.

It quickly became apparent that this category text had to be Retained in a file between sessions. What's more, other users might create and share their category note. So it must be possible to read category lists from Diverse Sources, like other websites. The reading and writing is now in the separate com.physpics.tools.stringmap package. Policy and user interaction are still part of FontViewer itself.

The Retention problem is thorny; after installation, how does the program get initial contents for the file? where does it store changes? and how does it find previously stored changes? Java and operating systems offer tools that can implement these policies, but none dictates a solution. Various applications choose the user's home directory, Window's registry, Mac's /Library/Preferences, or something similar. For FontViewer I decided the standard location for the category data is FontViewer/fontcategories.txt in the user's home directory. In addition, a menu option can read category data from Diverse Sources.

To provide Diverse Sources, StringMap can read from local files. the web (http:), literals (data:), and whatever other schemas are implemented in the Java library. Additionally StringMap implements schemas for reading from the uer's home directory (userhome:), the jar file (resource:), the directory conntaining the jar file (jardir:). StringMap can save the data to local files, userhome:, and the jar directory (jardir:).

Most of the feature described in this subset are devoted to providing the appropriate options to the user.

reading categories

reacting to changes to the data

saving the data

 

Save the categories list when the user has changed one or more of the entries

catschanged

StringMap does not provide a view and is not directly involved with displaying or saving the category data. Instead the event of a change to the data is harnessed to send the data on to the StringMap object. The handler is registered with the table model with the single line of code:

table.tableData.addTableModelListener(table.modelListener)

modelListener has earlier been assigned a TableModelListener object as a lambda providing the single required method:

TableModelListener modelListener = (TableModelEvent e) -> { ... }

The method checks that the change is to the category data and, if so, calls the StringMap object to record the new data.

When a new categories file is loaded, there might be many changes to the categories map. As a short cut, the model listener is removed during that process and restored at the end. Then all changes are processed at once with a single call:

tableData.fireTableDataChanged();

This provision is a case of over-thinking the design; StringMap does not save to disk every change; it waits until a minute after the latest change before it commits to disk. So the only overhead avoided is resetting the timer.

Read the categories data deliivered with the distribution

catsinitjardir

The catsinit feature loads initial contents for the category column. It fetches information from a number of possible sources so as to get the latest information from the web and the various files on the local platform.

This feature, catsinitjardir, adds to the list of sources the file fontcategories.txt in the same directory as FontViewer.jar. This location makes the font categories available to all users on the same local file system. It is placed first on the list of sources so its values are can be overriden by more recent information on the website (http://physpics.com/Java/apps/fontviewer/fontcategories.txt) and more localized information in the user's own files (userhome:/FontViewer/fontcategories.txt).

StringMap gets the jar directory by calling com.physpics.tools.ui.IOUtils: jarDir.

 

Prompt with a filechooser for an additional category file to load

catsreadui

The catsreadui feature adds the File->Load categories... menu item which prompts the user for a source location and passes it to StringMap.read. Since javax.swing.JFileChooser can only choose among local files, it does not support the full range of sources available through StringMap; so Load categories resorts to JOptionPane.

JOptionPane query for category data location
Object proposedLocation = JOptionPane.showInputDialog(this, msg,
    "Choose categories source", JOptionPane.QUESTION_MESSAGE, ICON64, null, ""); 
if (((proposedLocation instanceof String)
        && loadCat((String)proposedLocation)))
    mainTable.replaceCategories();

Originally I used a JLabel to create the query msg. It looked great with bold and italics and all:

JLabel version of msg

The downside of this choice was that the user could not copy and paste from the text. So switched to a JTextArea. For consistency with other option boxes, I copied the label font and background for the msg area. The label font can be gotten from (new JLabel()).getFont() or directly from the UIManager:

UIManager.getFont("Label.font")
Write category data to a file

catssave

When the user edits the category data in the left hand columns, the changes are preserved by the catssave feature. Since the actual writing of the data is done by StringMap, the code of the feature need only set policy. The policies of where and when are dictated by

public boolean setSaveFile(String fnm) { }

Parameter fnm is the save location policy. It is passed to StringMap with

if (catsMap.setMapDest(fnm) == StringMap.Dest.FAILED)
    return false; 

Initialization code calls this method with fnm set to the default,

userhome:/FontViewer/fontcategories.txt

The SaveAs menu item calls this same method, with fnm set to the user's chosen value. Note that FontViewer itself does not keep track of the save destination; StringMap retains it after the call to setMapDest.

A second policy decision is whether to save changes as they are made or wholesale at the user's request. I chose the first option for FontViewer. The choice is communicated to StringMap with the autosave feature as implemented by a single line in setSaveFile:

catsMap.setAutosaving(true); 

To reduce disk traffic, StringMap waits briefly to see if another change is made; then a single save will preserve both changes. The wait time defaults to 60 seconds, but can be changed by a call to setDeferSave(). A second timer ensures that the data will be saved even if the user continuously changes the category data. This timer defaults to ten minutes.

In additon to autosave, FontViewer has a menu option to save on demand, even though it seldom does anything. This item is enabled/disabled by setSaveFile according to whether StringMap succeeds in setting the save destination. The menu item is defined via createAction, where he lambda defining the action to be taken is

 ()->{ mainTable.finishEditing(); catsMap.save(); }

The call to finishEdit is important. It is common for a user to make a change and then select a menu option. But until the focus is redirected, the cell can remain open, without its change having been noticed and passed to the StringMap by the tableModelListener.

One final facet of the catssave feature is that it adds text to the message displayed for the Help->About menu item:

File dest = catsMap.getMapDest();
msg += (dest != null)            
		? "Now saving to "+ dest.getAbsolutePath() 
       : "NOT SAVING CATEGORY DATA."   

Here again we see that the destination is retained in the StringMap and not in FontViewer itself.

 

Prompt for a file to save category data

catssaveas

The catssaveas feature is mostly vanilla: a menu option that calls a file-chooser and then calls StringMap.setMapDest to enact the new save location. The code is more than doubled by adding a small accessory to the file-chooser. It suggests the userhome: and jardir: destinations.

The Action object for SaveAs is created (by the action  feature), added to the menu, and enabled with this code:

fileMenu.add(saveasAction=createAction(..., "Save as ...", ...);

saveasAction.setEnabled(true);
The last parameter to createAction is a lambda giving the behavior for choosing SaveAs:

()->{
    mainTable.finishEditing();
    promptForDest();
}





 

PromptForDest() is also called by the shutdown code in the unusual case that unsaved map changes exist, no map save destination has bneen provided, and the user responds to a query that they do want to save the changes. (That query is over-design. The user should be asked directly where to save the file.)

In  outline, promptForDest first chooses a default save location as the first local file found among the latest save destination, the latest read source, or ./filecategories.txt. (The current directory (./) is probably a poor choice, but there are no good choices once we've eliminated the source and destination options..) Next a JFileChooser is created and initialized  with the default save location and various user-friendly prompts and messages. Finally the chooser is shown to the user:

if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
    return false;

In the fall-thru case, the selected file is fetched via chooser.getSelectedFile(); it is made into a full, usable path; and it is passed to StringMap via return setSaveFile(sChosen), as described with feature catssave.

The file-chooser displayed to the user looks like this;

TODO TODO TODO file-chooser image

feature_header("catssaveacc");

The two button "accessory" shown in the middle right of the file-chooser image requires considerable additional code. Each button is generated with a call to

JButton createStdSave(JFileChooser chooser,            
    String name, File location) { ... }

This method creates a JButton displaying the given name. Its action, when clicked, is to pass the given location as the value for the given chooser. The default value for the first button comes from the user.home property provided by Java. The second comes from com.physpics.tools.ui.IOUtils.jarDir(FontViewer.class). The accessory itself is a Box.createVerticalBox() with the two buttons as content. The box is passed to the file-chooser via chooser.setAccessory(accessory).

 

Save categories data before shutting down

catsshut

As discussed in the shut feature, while the FontViewer window is closing the WindowAdapter's saveAndExit method is called. Other than calling finishEditing on the fonts table, this method is devoted to the catsshut feature. Ideally, the feature's code would be

if (catsMap.isChangesPending()) {
    if (catsMap.getMapDest() != null)
        catsMap.save();
    else promptForDest();
}

In other words, if there are no pending changes to the StringMap, the exit proceeds. If changes  are pending, and if there is a save destination, the changes are saved and the exit proceeds. But if there is no established destination, the user gets prompted for a save location and the changes are saved there. In this ideal scenario, JFileChooser would offer three buttons: Save, Don't save, and Cancel, where the third option would forestall the window exit. (See the shut feature for stopping the window exit.)

Sadly, JFileChooser must have exactly zero or two buttons, with one one labeled Cancel.

My reluctant design choice was to first prompt for Save/Don't save/Cancel and then show the file-chooser for the Save choice. The askuser feature makes this possible by defining an array of button names and acting on whichever button gets clicked. Without askuser, JOptionPane.showInputDialog can be called to get the user's choice.

TODO TODO TODO images of three-button close dialog and file-chooser widget

Index of Java Methods Usage

Package Method Features where the code calls the method
javax/swing ­Abstract­Action action
javax/swing ­Action catsreadui  catssave  webstart
java/awt/event ­Action­Listener action
javax/swing ­Action­Map resettext
java/util ­Array­List about  catsload  logoimages  sort
java/lang ­Boolean checkboxes
javax/swing ­Border­Factory listwindow  table
javax/swing ­Box titled  top
java/awt/image ­Buffered­Image sortbywidth
javax/swing ­Cell­Editor finishedit
java/lang ­Character keystyles
java/lang ­Class about  logoimages  webstart
java/awt/datatransfer ­Clipboard fillcut
java/util ­Collections bettersort
javax/swing/text ­Default­Caret edpresel
javax/swing/table ­Default­Table­Model catschanged  catsload  seetext  styles  table
java/awt ­Desktop guide
java/awt ­Event­Queue shut
java/io ­File catssave
java/awt ­Font catschanged  catsload  offline  seetext  table
java/awt ­Font­Metrics sortbywidth
java/awt ­Graphics bettersort  scrollcorner  sortbywidth
java/awt ­Graphics­Environment fontlist
javax/swing ­Input­Map resettext
java/lang ­Integer action
java/util ­Iterator bettersort  catsload
javax/swing ­J­Button adjustlaf  catsreadui  catssaveas  catsshut
javax/swing ­J­Combo­Box pickstyle  seetext
javax/swing ­J­Component titled
javax/swing ­J­Dialog askuser
javax/swing ­J­Frame listwindow  logoimages  menu  shut
javax/swing ­J­Label seetext  titled
javax/swing ­J­Menu about  catsreadui  catssave  catssaveas  ccpaction  guide  menu  resettext  shut
javax/swing ­J­Menu­Bar ccpaction  menu
javax/swing ­J­Menu­Item ccpaction  resettext
javax/swing ­J­Option­Pane askuser
javax/swing ­J­Popup­Menu resettext
javax/swing ­J­Scroll­Bar scroll
javax/swing ­J­Scroll­Pane rowvis  scroll  scrollcorner
javax/swing ­J­Spinner picksize  seetext
javax/swing/table ­J­Table­Header bettersort  table
javax/swing ­J­Text­Field catsreadui  catssaveas  catsshut  celled  seetext  telluser
javax/swing ­J­Viewport rowvis
java/awt/event ­Key­Event action  ccpaction  keystyles
javax/swing ­Key­Stroke action  ccpaction  resettext
java/util ­List bettersort
java/lang ­Math rowheight  titled
java/awt/event ­Mouse­Event coltooltips
java/lang ­Number picksize
java/lang ­Object about  askuser  bettersort  categories  catsload  catssave  celled  fonts  seetext  sort  styles
java/lang ­Package about
java/io ­Print­Stream offline
java/awt/geom ­Rectangle2­D sortbywidth
javax/swing ­Spinner­Number­Model picksize
java/lang ­String action  catsload  catssave  ccpaction  edpresel  keystyles  seetext  sort  styles
java/lang ­String­Builder about  askuser  bettersort  catsload  catssave  fillcut  guide  keystyles  logoimages  styles  telluser  titled  webstart
javax/swing ­Swing­Utilities bettersort  listwindow
javax/swing/table ­Table­Column coltooltips  table
javax/swing/table ­Table­Column­Model coltooltips
javax/swing/table ­Table­Model catschanged  keystyles  styles
javax/swing/event ­Table­Model­Event catschanged  styles
javax/swing/table ­Table­Row­Sorter bettersort
javax/swing ­Tool­Tip­Manager adjustlaf
java/awt ­Toolkit backbone  fillcut  logoimages  shut
javax/swing ­U­I­Manager adjustlaf
java/net ­U­R­I logoimages
java/awt ­Window shut
java/awt/event ­Window­Event shut
java/awt/event/ActionListener action­Performed action
java/util/ArrayList add about  logoimages  sort
java/util/List add bettersort
javax/swing/Box add titled  top
javax/swing/JMenu add about  catsreadui  catssave  catssaveas  ccpaction  guide  resettext  shut
javax/swing/JMenuBar add ccpaction  menu
javax/swing/JPopupMenu add resettext
javax/swing/JButton add­Action­Listener catsreadui  catssaveas  catsshut
javax/swing/JSpinner add­Change­Listener seetext
javax/swing/table/DefaultTableModel add­Column table
javax/swing/JComboBox add­Item­Listener seetext
javax/swing/table/DefaultTableModel add­Table­Model­Listener catschanged
javax/swing/JFrame add­Window­Listener shut
java/lang/StringBuilder append about  askuser  bettersort  catsload  catssave  fillcut  guide  keystyles  logoimages  styles  telluser  titled  webstart
java/awt/Desktop browse guide
java/lang/String char­At action  ccpaction
java/lang/Boolean compare­To checkboxes
java/lang/String compare­To­Ignore­Case sort
java/awt/event/KeyEvent consume keystyles
java/lang/String contains styles
javax/swing/BorderFactory create­Compound­Border table
javax/swing/JOptionPane create­Dialog askuser
javax/swing/BorderFactory create­Empty­Border listwindow  table
javax/swing/Box create­Horizontal­Box top
javax/swing/Box create­Horizontal­Strut top
java/awt/Toolkit create­Image logoimages
javax/swing/BorderFactory create­Line­Border table
java/awt/Font derive­Font seetext
java/awt/Window dispose shut
javax/swing/JDialog dispose askuser
java/awt/Graphics draw­Line bettersort  scrollcorner
java/lang/String ends­With keystyles
java/lang/Object equals askuser
java/lang/String equals seetext
java/util/List equals bettersort
javax/swing/JTextField fire­Action­Performed seetext
javax/swing/table/DefaultTableModel fire­Table­Cell­Updated seetext
javax/swing/table/DefaultTableModel fire­Table­Data­Changed catschanged
java/lang/Class for­Name webstart
java/util/ArrayList get sort
java/util/List get bettersort
java/io/File get­Absolute­Path catssave
java/awt/GraphicsEnvironment get­All­Fonts fontlist
java/lang/Object get­Class about  bettersort  categories  catsload  catssave  celled  fonts  seetext  sort  styles
javax/swing/event/TableModelEvent get­Column catschanged  styles
javax/swing/table/TableColumnModel get­Column coltooltips
javax/swing/table/DefaultTableModel get­Column­Count table
javax/swing/table/TableColumnModel get­Column­Index­At­X coltooltips
java/awt/Toolkit get­Default­Toolkit backbone
java/awt/Desktop get­Desktop guide
java/awt/event/KeyEvent get­Extended­Key­Code­For­Char action  ccpaction
javax/swing/event/TableModelEvent get­First­Row catschanged
java/awt/Graphics get­Font­Metrics sortbywidth
java/awt/Font get­Font­Name catschanged  catsload  offline  table
java/awt/FontMetrics get­Font­Render­Context sortbywidth
java/awt/image/BufferedImage get­Graphics sortbywidth
java/lang/Package get­Implementation­Version about
java/awt/event/KeyEvent get­Key­Code keystyles
javax/swing/KeyStroke get­Key­Stroke action  ccpaction  resettext
java/awt/GraphicsEnvironment get­Local­Graphics­Environment fontlist
javax/swing/table/TableColumn get­Model­Index coltooltips
javax/swing/SpinnerNumberModel get­Number picksize
java/lang/Class get­Package about
java/awt/event/MouseEvent get­Point coltooltips
javax/swing/JComponent get­Preferred­Size titled
javax/swing/JLabel get­Preferred­Size titled
java/lang/Class get­Resource logoimages
javax/swing/JComboBox get­Selected­Index pickstyle
javax/swing/event/TableModelEvent get­Source catschanged
java/awt/Toolkit get­System­Clipboard fillcut
java/awt/Toolkit get­System­Event­Queue shut
javax/swing/JTextField get­Text catsreadui  catssaveas  catsshut  celled  telluser
javax/swing/event/TableModelEvent get­Type catschanged
javax/swing/JOptionPane get­Value askuser
javax/swing/table/TableModel get­Value­At catschanged  keystyles  styles
javax/swing/JScrollPane get­Vertical­Scroll­Bar scroll
javax/swing/JViewport get­View­Rect rowvis
javax/swing/JScrollPane get­Viewport rowvis
java/awt/geom/Rectangle2D get­Width sortbywidth
java/awt/event/WindowEvent get­Window shut
java/util/Iterator has­Next bettersort  catsload
java/lang/String index­Of action  catsload
java/lang/Number int­Value picksize
javax/swing/SwingUtilities invoke­Later bettersort  listwindow
java/awt/Desktop is­Desktop­Supported guide
java/lang/String is­Empty catssave
java/lang/Character is­Letter­Or­Digit keystyles
java/awt/Desktop is­Supported guide
java/util/ArrayList iterator catsload
java/util/List iterator bettersort
java/lang/String length edpresel  keystyles  sort
java/lang/Math max rowheight  titled
java/util/Iterator next bettersort  catsload
javax/swing/JFrame pack listwindow
javax/swing/table/JTableHeader paint­Immediately bettersort
java/awt/EventQueue post­Event shut
java/io/PrintStream println offline
javax/swing/ActionMap put resettext
javax/swing/InputMap put resettext
javax/swing/UIManager put adjustlaf
javax/swing/AbstractAction put­Value action
javax/swing/table/DefaultTableModel remove­Table­Model­Listener catschanged
java/lang/String replace action
java/util/ArrayList set sort
javax/swing/JMenuItem set­Accelerator ccpaction  resettext
javax/swing/JDialog set­Always­On­Top askuser
javax/swing/table/JTableHeader set­Background table
javax/swing/JLabel set­Border seetext
javax/swing/JFrame set­Bounds listwindow
javax/swing/JFrame set­Content­Pane listwindow
java/awt/datatransfer/Clipboard set­Contents fillcut
javax/swing/JScrollPane set­Corner scrollcorner
javax/swing/JFrame set­Default­Close­Operation shut
javax/swing/table/JTableHeader set­Default­Renderer bettersort
javax/swing/ToolTipManager set­Dismiss­Delay adjustlaf
javax/swing/JMenuItem set­Displayed­Mnemonic­Index ccpaction
javax/swing/text/DefaultCaret set­Dot edpresel
javax/swing/Action set­Enabled catsreadui  catssave  webstart
javax/swing/JButton set­Font adjustlaf
javax/swing/JComboBox set­Font pickstyle
javax/swing/JLabel set­Font seetext
javax/swing/JSpinner set­Font picksize
javax/swing/JLabel set­Foreground seetext  titled
javax/swing/JFrame set­Icon­Images logoimages
javax/swing/ToolTipManager set­Initial­Delay adjustlaf
javax/swing/JFrame set­J­Menu­Bar menu
javax/swing/table/TableColumn set­Max­Width table
javax/swing/table/TableColumn set­Min­Width table
javax/swing/JMenu set­Mnemonic ccpaction  menu
javax/swing/JMenuItem set­Mnemonic ccpaction
javax/swing/table/TableRowSorter set­Sort­Keys bettersort
javax/swing/JLabel set­Text seetext
javax/swing/JMenuItem set­Text ccpaction
javax/swing/JTextField set­Text celled
javax/swing/JScrollBar set­Value scroll
javax/swing/table/DefaultTableModel set­Value­At catsload  seetext  styles
javax/swing/table/TableModel set­Value­At keystyles
javax/swing/JViewport set­View­Position rowvis
javax/swing/JScrollPane set­Viewport­View scroll
javax/swing/JDialog set­Visible askuser
javax/swing/JFrame set­Visible listwindow
javax/swing/ToolTipManager shared­Instance adjustlaf
java/util/ArrayList size about  sort
java/util/List size bettersort
java/lang/String starts­With keystyles
javax/swing/CellEditor stop­Cell­Editing finishedit
java/lang/String substring catsload  keystyles  styles
java/lang/StringBuilder to­String about  askuser  bettersort  catsload  catssave  fillcut  guide  keystyles  logoimages  styles  telluser  titled  webstart
java/net/URI to­U­R­L logoimages
java/lang/String trim catsload  styles
java/util/Collections unmodifiable­List bettersort
java/lang/Boolean value­Of checkboxes
java/lang/Integer value­Of action
 
Copyright © 2024 ZweiBieren, All rights reserved. Dec 12, 2024 07:03 GMT Page maintained by ZweiBieren