|
Code Notes
See help file for how to use the source explorer window.
Code essential to being an executable program
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
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
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
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 Font s 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
no description
Table column that shows the font name for each row
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
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
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
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
no description
An editable TextField to edit the sample text shown in the Samples column of the table
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
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
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
no description
Adds a column of checkboxes; sorting on it brings candidates together
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
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
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)
no description
moving columns triggers sort;
sigh - need sort buttons
Sort the table on the category or fontname column
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
no description
Menu item to show information about the application and its authors
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, 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. 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.

A method to create Action objects from multiple arguments
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 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
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:
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:
Note the use of <htlm> to get a newline into the text.
Open the browser viewing the FontViewer help text.
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
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
}
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);
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
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 JFrame s. 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 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
no description
Table column showing a category for each font
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.
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 . 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
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
no description
Load categories data
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
no description
The first two columns show I for Italic and B for Bold
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):

left side of table, "BI" and "Category" columns
Additional user options
no description
Display a dialog box with multiple response buttons
no description
Automatically copy a clicked-on fontname to the cut buffer
no description
Edit cells in the categories column
no description
Menu items for cut/copy/paste
no description
Select the entire contents of a cell when opening it for editing
no description
Ensure closure of the editing category item if menu actions occur
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 …"
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
 |
|
 |
"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
no description
Numerous tweaks to improve the user experience
no description
Tweak a few global look and feel parameters
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;
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
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
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
no description
When user types 'B' or 'I', toggle bold or italic column
no description
Display the FontViewer logo on the window decoration and in dialog boxes
no description
Adjust the height of table rows depending on fontsize
no description
When the size or style changes, scroll back to the previous view
no description
Draw the corner at the intersection of the header and the scrollbar
no description
Sort the samples column by the width of the sample in that font
no description
Display a title for the sizer and styler boxes above the table
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);
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
no description
Add the user interfacefor saving the category data to a file
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
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
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
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 .
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:
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
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
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
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
|
|