> Java > tutorials > FontViewerSource
by Zwei bier en Source Code for FontViewer FontViewer Logo
        
COPY
Select ALL
     
Grab URL
× Headers
Help Runnable
















































// FontViewer.java - Display system fonts, with a sample in each// 0 
// FontViewer preface - comment, package, imports// 1 
/**   FontViewer.java// 2 comments
* Show all the fonts that Java knows// 3 comments
* and display a sample string in each// 4 comments
*// 5 comments
* @author zweibieren// 6 comments
*  Jan 13, 2010 5:25 PM// 7 comments
*/// 8 comments
// 9 comments
package com.physpics.fontviewer;// 10 backbone
// 11 backbone
import java.io.*;// 12 readcats|offline
import java.util.*;// 13 stringmap|sort
import java.awt.*;// 14 fontlist
import javax.swing.*;// 15 backbone
import java.awt.event.*;// 16 window
import java.net.*;// 17 help
import javax.swing.table.*;// 18 table
import java.util.regex.*;// 19 readcats|styles
// 20 window
// end FontViewer preface - comment, package, imports// 21 
// FontViewer class// 22 
/**// 23 comments
* View a string in all fonts on the system.// 24 comments
* The window is laid out like this <pre>// 25 comments
*    S C TTT// 26 comments
*    LLLLLL!// 27 comments
*    LLLLLL!// 28 comments
*    LLLLLL!// 29 comments
*// 30 comments
* S, a JSpinner for choosing a font size// 31 comments
* C, a JComboBox for choosing one of the four styles// 32 comments
* TT, A JTextField for writing the text to be displayed in the fonts// 33 comments
* LLL, a JTable where each row has:// 34 comments
* "B" if font is bold// 35 comments
* "I" if font is italic// 36 comments
* font category, e.g., script or serif// 37 comments
* a check box to mark candidate fonts// 38 comments
* font name// 39 comments
* the T text displayed in the named font// 40 comments
*  ! there is a vertical scrollbar for LLL// 41 comments
*     </pre>// 42 comments
*/// 43 comments
public class FontViewer extends JPanel {// 44 backbone
public class FontViewer extends JPanel implements ActionListener {// 45 backbone&menu
// 46 backbone
// FontViewer class variable declarations - globals for the visible window components// 47 
    static final Font[] fontList;   // names of all fonts known to Java// 48 fontlist
    static {// 49 fontlist
GraphicsEnvironment gEnv =// 50 fontlist
GraphicsEnvironment.getLocalGraphicsEnvironment();// 51 fontlist
fontList = gEnv.getAllFonts();// 52 fontlist
    }// 53 fontlist
// 54 fontlist
    static final String defaultSample // default string  for fontsamples// 55 sampletext|samples
    // has all letters, two ligatures, common digrams & trigrams// 56 sampletext|samples
    = "Fred fixed the zoo flight's problems "// 57 sampletext|samples
    + "by quickly waiving his objections.";// 58 sampletext|samples
    String currentSample = defaultSample;// 59 sampletext|samples
// 60 sampletext|samples
    // default location to read categories// 61 readcats
    static final String catsDefaultFile = "resource:fontcategories.txt";// 62 readcats
// 63 readcats
    static final Font labelFont // default font for screen text// 64 window
    = new Font("Dialog", Font.PLAIN, 14);// 65 window
    static final Font headerFont //  font for column headers// 66 table
    = new Font("Dialog", Font.BOLD, 14);// 67 table
    static final String[] styleNames //  for the style JComboBox// 68 pickstyle
    = {"Plain", "Bold", "Italic", "Bold Italic"};// 69 pickstyle
    static final int[] styleValues// 70 pickstyle
    = new int[]{Font.PLAIN, Font.BOLD,// 71 pickstyle
Font.ITALIC, Font.BOLD | Font.ITALIC};// 72 pickstyle
    final static int INITIALFONTSIZE = 12;  // initial fontsize for samples// 73 picksize|table
// 74 table
    // table row height is fontsize plus ADDFORROWHEIGHT// 75 table
    final static int ADDFORROWHEIGHT = 8;// 76 table
// 77 table
    // components of the window// 78 window
    JComboBox styles = new JComboBox(styleNames);// 79 pickstyle
    SpinnerNumberModel sizeModel // font sizes range// 80 picksize
    = new SpinnerNumberModel(INITIALFONTSIZE, 6, 40, 4);// 81 picksize
    JSpinner sizes;     // choose from sizeModel// 82 picksize
// 83 picksize
    // the fonts listing// 84 table
    FontsTable mainTable;    // one line per font// 85 table
    JScrollPane scrollTable;  // scroller for mainTable// 86 scroll|rowvis
    int styleColIndex = -1;  // STYLECOL index for style flags, B/I// 87 styles
    int catColIndex = -1;     // CATCOL  index for categories// 88 cats
    int fontColIndex = -1; // FONTCOL  index for font names// 89 fonts
    int chkColIndex = -1; // CHECKCOL  index for checkboxes// 90 checkboxes
    int sampleColIndex = -1;   // SAMPLECOL  index for sample text// 91 samples
    JTextField sampleEditor;// 92 samples
    DemoText sampleEditor; // editor for the sample text// 93 sampletext
// 94 table
    // borders for cells in the table// 95 table
    static final javax.swing.border.Border cellBorder// 96 table
    = BorderFactory.createEmptyBorder(0, 3, 0, 0);// 97 table
    static final javax.swing.border.Border borderBlue// 98 table
    = BorderFactory.createCompoundBorder(// 99 table
    BorderFactory.createLineBorder(Color.BLUE),// 100 table
    BorderFactory.createEmptyBorder(0, 2, 0, 0));// 101 table
// 102 table
    // a row is offscreen when no more than SLIVER pixels are visible// 103 rowvis
    final static int SLIVER = 6;// 104 rowvis
// 105 rowvis
    // double buffering is turned off during fast scroll// 106 fasterscroll
    boolean needToRestoreDoubleBuffering = false;// 107 fasterscroll
// 108 fasterscroll
    javax.swing.Timer saveTask = null;// 109 savecats&autosave
// 110 savecats&autosave
// end FontViewer class variable declarations - globals for the visible window components// 111 
// FontViewer() constructor: create the window components// 112 
    /** Construct a FontViewer.// 113 window
     * @param catMap  Map from font names to categories// 114 window&stringmap
     * This will establish values for CATCOL.// 115 window&stringmap
     */// 116 window
    private FontViewer() {// 117 offline
    public FontViewer() {// 118 window
    public FontViewer(StringMap catMap) {// 119 window&stringmap
// make the size chooser for the top row// 120 picksize
sizes = new JSpinner(sizeModel);// 121 picksize
sizes.setFont(labelFont);// 122 picksize
sizes.addChangeListener(// 123 picksize&samples
new javax.swing.event.ChangeListener() {// 124 picksize&samples
    @Override// 125 picksize&samples
    public void stateChanged(javax.swing.event.ChangeEvent e) {// 126 picksize&samples
mainTable.refontTheSamples();// 127 picksize&samples
    }// 128 picksize&samples
});// 129 picksize&samples
// 130 picksize
// make the style chooser for the top row// 131 pickstyle
styles.setFont(labelFont);// 132 pickstyle
styles.addItemListener(new ItemListener() {// 133 pickstyle&samples
    @Override// 134 pickstyle&samples
    public void itemStateChanged(ItemEvent e) {// 135 pickstyle&samples
mainTable.refontTheSamples();// 136 pickstyle&samples
    }// 137 pickstyle&samples
});// 138 pickstyle&samples
// 139 pickstyle
// make the sample text editor// 140 sampletext
sampleEditor = new JTextField(defaultSample);// 141 samples
sampleEditor = new DemoText(defaultSample);// 142 sampletext
// 143 sampletext
// ENTER key in sampletext: so rebuild the SAMPLECOL// 144 sampletext&samples
sampleEditor.addActionListener(new ActionListener() {// 145 sampletext&samples
@Override// 146 sampletext&samples
public void actionPerformed(ActionEvent e) {// 147 sampletext&samples
    mainTable.reviseSamples();// 148 sampletext&samples
}// 149 sampletext&samples
});// 150 sampletext&samples
// 151 sampletext&samples
// make the table showing fonts// 152 table
scrollTable = new JScrollPane(// 153 scroll|rowvis
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,// 154 scroll|rowvis
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) {// 155 scroll|rowvis
};// 156 scroll|rowvis
// 157 fasterscroll
scrollTable.setViewport(new JViewport() {// 158 fasterscroll
    @Override// 159 fasterscroll
    public void paint(Graphics g) {// 160 fasterscroll
super.paint(g);// 161 fasterscroll
if (needToRestoreDoubleBuffering) {// 162 fasterscroll
    needToRestoreDoubleBuffering = false;// 163 fasterscroll
    FontViewer.this.setDoubleBuffered(true);// 164 fasterscroll
    FontViewer.this.getRootPane().setDoubleBuffered(true);// 165 fasterscroll
}// 166 fasterscroll
    }// 167 fasterscroll
});// 168 fasterscroll
mainTable = new FontsTable();// 169 table
mainTable = new FontsTable(catMap);// 170 table&readcats
scrollTable.setViewportView(mainTable);// 171 scroll|rowvis
// 172 scroll|rowvis
scrollTable.setCorner(ScrollPaneConstants.UPPER_RIGHT_CORNER,// 173 scrollcorner
mainTable. new FTCorner());// 174 scrollcorner
mainTable.refontTheSamples();// 175 samples
// 176 samples
scrollTable.getVerticalScrollBar().getModel()// 177 fastscroll
    .addChangeListener(mainTable.new FTChangeListener());// 178 fastscroll
// 179 fastscroll
// put things in the window// 180 window
setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));// 181 window
setLayout(new BorderLayout(6, 6));  // gaps of size 6// 182 window
Box top = Box.createHorizontalBox();// 183 top
top.add(Box.createHorizontalStrut(12));// 184 top
sizes.setBorder(BorderFactory.createTitledBorder("Font Size"));// 185 picksize
top.add(sizes);// 186 picksize
top.add(new TitledWidget("Font Size", sizes, true));// 187 picksize&titled
top.add(Box.createHorizontalStrut(12));// 188 top
styles.setBorder(BorderFactory.createTitledBorder("Font Style"));// 189 pickstyle
top.add(styles);// 190 pickstyle
top.add(new TitledWidget("Font Style", styles, true));// 191 pickstyle&titled
top.add(Box.createHorizontalStrut(12));// 192 top
sampleEditor.setBorder(BorderFactory.createTitledBorder("Sample Text"));// 193 sampletext
top.add(sampleEditor);// 194 sampletext
top.add(new TitledWidget("Sample Text", sampleEditor, false));// 195 sampletext&titled
top.add(Box.createHorizontalStrut(12));// 196 top
// 197 top
add(top, BorderLayout.NORTH);// 198 top
add(mainTable, BorderLayout.CENTER);// 199 table
add(scrollTable, BorderLayout.CENTER);// 200 table&scroll
    } // end FontViewer(...)// 201 offline
    } // end FontViewer(...)// 202 window
// 203 window
// end FontViewer() constructor: create the window components// 204 
// FontViewer.actionPerformed handle menu hits //=menu// 205 
    public void actionPerformed(ActionEvent e) {// 206 menu
switch(e.getActionCommand().charAt(0)) {// 207 menu
case 'S': // Save// 208 menu
    mainTable.cancelEditing();// 209 menu&canceledit
    if (mainTable.catsMap.catDest == null// 210 menu&filechooser
    || ! mainTable.catsMap.mutable)// 211 menu&filechooser
mainTable.catsMap.promptForCatDest();// 212 menu&filechooser
    if (mainTable.catsMap.catDest != null)// 213 menu&filechooser
mainTable.saveCats();// 214 menu&savecats
    break;// 215 menu
case 'A': // SaveAs// 216 menu
    mainTable.cancelEditing();// 217 menu&canceledit
    mainTable.catsMap.promptForCatDest();// 218 menu&filechooser
    if (mainTable.catsMap.catDest != null)// 219 menu&filechooser
mainTable.saveCats();// 220 menu&savecats
    break;// 221 menu
case 'X':// 222 menu&shut
    Container top = getTopLevelAncestor();// 223 menu&shut
    if (top instanceof Window)// 224 menu&shut
((Window)top).dispose();  //will throw windowClosing event// 225 menu&shut
    break;// 226 menu&shut
case 'H': // Help// 227 menu&help
    Desktop dt = Desktop.getDesktop();// 228 menu&help
    try {// 229 menu&help
// FVHelp.html is in the same directory as FontViewer.jar// 230 menu&help
    // url is in form:  file://path/FontViewer.jar!/com/...// 231 menu&help
    // desired URI is file://path/FVHelp.html// 232 menu&help
String path = FontViewer.class// 233 menu&help
.getResource("FontViewer.class").getPath();// 234 menu&help
String uri;// 235 menu&help
int bangX = path.indexOf('!');// 236 menu&help
if (bangX >= 0) {// 237 menu&help
    // the usual case where FontViewer.class is in a jar// 238 menu&help
    int slashX = path.substring(0, bangX).lastIndexOf('/');// 239 menu&help
    uri = path.substring(0,slashX);// 240 menu&help
}// 241 menu&help
else // not in a jar; probably debugging; use server version// 242 menu&help
    uri = "http://physpics.com/Java/apps/FontViewer";// 243 menu&help
dt.browse(new URI(uri+"/FVHelp.html"));// 244 menu&help
    }// 245 menu&help
    catch(IOException ioe) { ioe.printStackTrace(); }// 246 menu&help
    catch(URISyntaxException use) { use.printStackTrace(); }// 247 menu&help
    break;// 248 menu&help
}// 249 menu
    }// 250 menu
// 251 menu
// end FontViewer.actionPerformed handle menu hits //=menu// 252 
// FontViewer.startSaving(): xreate periodic save task// 253 
    /**// 254 savecats&autosave
     * Start a thread that will periodically dispatch a save-categories task.// 255 savecats&autosave
     * It continues even if the StringMap is neither mutable nor changed;// 256 savecats&autosave
     * its resource burden is quite small.// 257 savecats&autosave
     * @param interval  milliseconds between save tries// 258 savecats&autosave
     */// 259 savecats&autosave
    public void startSaving(int interval) {// 260 savecats&autosave
        if (saveTask == null) {// 261 savecats&autosave
ActionListener saveCatsTask= new ActionListener() {// 262 savecats&autosave
    @Override// 263 savecats&autosave
    public void actionPerformed(ActionEvent evt) {// 264 savecats&autosave
if (mainTable.catsMap.changed// 265 savecats&autosave
&& mainTable.catsMap.catDest != null)// 266 savecats&autosave
mainTable.saveCats();// 267 savecats&autosave
    }// 268 savecats&autosave
};// 269 savecats&autosave
saveTask = new javax.swing.Timer(interval, saveCatsTask);// 270 savecats&autosave
    }// 271 savecats&autosave
    saveTask.start();// 272 savecats&autosave
    }// 273 savecats&autosave
    /**// 274 savecats&autosave
     * Stop(but do not discard) the task that is doing periodic saves.// 275 savecats&autosave
     */// 276 savecats&autosave
    public void stopSaving() {// 277 savecats&autosave
    saveTask.stop();// 278 savecats&autosave
    }// 279 savecats&autosave
// end FontViewer.startSaving(): xreate periodic save task// 280 
// FontViewer.printFontList(): print fontnames// 281 
    /**// 282 offline
     *   Print the list of fonts// 283 offline
     * @param ps where to send the output// 284 offline
     */// 285 offline
    static public void printFontList(PrintStream ps) {// 286 offline
for (Font f : FontViewer.fontList)// 287 offline
ps.println(f.getFontName());// 288 offline
    }// 289 offline
// 290 offline
// end FontViewer.printFontList(): print fontnames// 291 
// 292 comments
// 293 titled|sampletext
// 294 titled|sampletext
    ////////////////////////////////// 295 titled|sampletext
    // HELPER CLASSES// 296 titled|sampletext
// 297 titled|sampletext
// TitledWidget - class : A JPanel with a title at top and a widget within// 298 
    /**// 299 titled
     * Create a JPanel containing a widget with a title above it.// 300 titled
     * This class is here because look-and-feel implementations// 301 titled
     * differ too much in borders around JComboBox and JSpinner.// 302 titled
     */// 303 titled
    public class TitledWidget extends JPanel {// 304 titled
/**// 305 titled
* Create a TitledWidget with padding on the right and below.// 306 titled
* @param t   Title string to display// 307 titled
* @param widget The widget to border// 308 titled
* @param fixedWidth Prevent width change// 309 titled
*/// 310 titled
public TitledWidget(String t,// 311 titled
JComponent widget, boolean fixedWidth) {// 312 titled
    setLayout(new BorderLayout());   // layout for JPanel// 313 titled
    JLabel lbl = new JLabel(" "+t);  // to line up left edges// 314 titled
    lbl.setForeground(Color.BLUE);// 315 titled
    add(lbl, BorderLayout.NORTH);// 316 titled
    add(widget, BorderLayout.CENTER);// 317 titled
    if (fixedWidth) {// 318 titled
Dimension wpref = widget.getPreferredSize();// 319 titled
Dimension lpref = lbl.getPreferredSize();// 320 titled
Dimension pref = new Dimension(// 321 titled
Math.max(wpref.width, lpref.width),// 322 titled
wpref.height+lpref.height);// 323 titled
setMaximumSize(pref);// 324 titled
    }// 325 titled
}// 326 titled
    }// 327 titled
// end TitledWidget - class : A JPanel with a title at top and a widget within// 328 
// DemoText - class : a JTextField with application-specific event handlers and reset via mouse and keyboard// 329 
    /**// 330 sampletext
     * The sample text to be shown in each font.// 331 sampletext
     */// 332 sampletext
    class DemoText extends JTextField {// 333 sampletext
public final Action resetAction;// 334 resetaction
public final String resetItem = "reset action";// 335 resetkey
@Override// 336 sampletext&samples
public void fireActionPerformed() {// 337 sampletext&samples
    currentSample = getText();// 338 sampletext&samples
    super.fireActionPerformed();// 339 sampletext&samples
}// 340 sampletext&samples
public DemoText(String starterText) {// 341 sampletext
    super(starterText, 50);// 342 sampletext
    setFont(labelFont);// 343 sampletext
    setMargin(new Insets(3, 8, 3, 0));// 344 sampletext
// 345 sampletext
    // focus loss from sampletext: so rebuild all samples in table// 346 sampletext&samples
    addFocusListener(new FocusAdapter() {// 347 sampletext&samples
@Override// 348 sampletext&samples
public void focusLost(FocusEvent e) {// 349 sampletext&samples
    if ( ! getText().equals(currentSample))// 350 sampletext&samples
fireActionPerformed();// 351 sampletext&samples
}// 352 sampletext&samples
    });// 353 sampletext&samples
// 354 sampletext&samples
// DemoText().resetAction : provide mouse and keystroke actions to reset the field// 355 
    // ------------------------------------------------// 356 resetaction
    // the remainder of DemoText() defines menu and keystroke// 357 resetaction
    // to reset the sample text to its default value// 358 resetaction
// 359 resetaction
    // define the Action that will be invoked by key or menu// 360 resetaction
    resetAction = new AbstractAction("Reset") {// 361 resetaction
@Override// 362 resetaction
public void actionPerformed(ActionEvent e) {// 363 resetaction
    setText(defaultSample);// 364 resetaction
    currentSample = defaultSample;// 365 resetaction
    fireActionPerformed();// 366 resetaction
}// 367 resetaction
    };// 368 resetaction
    resetAction.putValue(AbstractAction.SHORT_DESCRIPTION,// 369 resetaction
    "Reset the text to its original test value.");// 370 resetaction
    resetAction.putValue(AbstractAction.MNEMONIC_KEY,// 371 resetaction
    new Integer(KeyEvent.VK_R));// 372 resetaction
    KeyStroke ctlR = KeyStroke.getKeyStroke(KeyEvent.VK_R,// 373 resetkey
    ActionEvent.CTRL_MASK);// 374 resetkey
    resetAction.putValue(AbstractAction.ACCELERATOR_KEY, ctlR);// 375 resetkey
// 376 resetaction
    // ctlR key will reset// 377 resetkey
//     getKeymap().addActionForKeyStroke(ctlR, resetAction);// 378 resetkey
    getActionMap().put(resetItem, resetAction);// 379 resetkey
    getInputMap().put(ctlR, resetItem);// 380 resetkey
// 381 resetkey
    // define and utilize a popup menu with only an item for reset// 382 resetmenu
    final JPopupMenu resetMenu = new JPopupMenu();// 383 resetmenu
    resetMenu.add(new JMenuItem(resetAction));// 384 resetmenu
    addMouseListener(new MouseAdapter() {// 385 resetmenu
void popup(MouseEvent e) {// 386 resetmenu
    resetMenu.show(sampleEditor, e.getX(), e.getY());// 387 resetmenu
}// 388 resetmenu
@Override// 389 resetmenu
public void mousePressed(MouseEvent e)// 390 resetmenu
{ if (e.isPopupTrigger()) popup(e); }// 391 resetmenu
@Override// 392 resetmenu
public void mouseReleased(MouseEvent e)// 393 resetmenu
{ if (e.isPopupTrigger()) popup(e); }// 394 resetmenu
    });// 395 resetmenu
// end DemoText().resetAction : provide mouse and keystroke actions to reset the field// 396 
} // end DemoText constructor// 397 sampletext
    } // end class DemoText// 398 sampletext
// 399 sampletext
// end DemoText - class : a JTextField with application-specific event handlers and reset via mouse and keyboard// 400 
// 401 table&comments
// 402 table&comments
    ///////////////////////////////////////// 403 table&comments
    // FontsTable// 404 table&comments
// 405 table&comments
// FontsTable - class : the central JTable for the application// 406 
    /**// 407 table
     * The table of font names and sample strings.// 408 table
     */// 409 table
    private class FontsTable extends JTable {// 410 table
DefaultTableModel tableData = new DefaultTableModel() {// 411 table
    @Override// 412 table
    // only CATCOL and CHECKCOL can be edited// 413 table
    public boolean isCellEditable(int row, int col) {// 414 table
    if (col == catColIndex) return true;// 415 celled
    if (col == catColIndex) return catsMap.mutable;// 416 celled&savecats
    if (col == chkColIndex) return true;// 417 checkboxes
    return false;// 418 table
    }// 419 table
};// 420 table
// 421 table
StringMap catsMap = null;// 422 stringmap
// 423 stringmap
final JTextField sink               //  way-station for paste value// 424 ctlv
= new JTextField();// 425 ctlv
final Action pasteSinkAction // action that does paste// 426 ctlv
= TransferHandler.getPasteAction();// 427 ctlv
// 428 ctlv
// colors for header background// 429 table
Color normalHeaderBkgd = new Color(255, 255, 0xCE);// 430 table
Color sortingHeaderBkgd = new Color(255, 0x99, 0x33);// 431 bettersort
// 432 table
// backdoor value for header cell renderer// 433 bettersort
int nowSorting = -1;     // what column (in Model) is being sorted// 434 bettersort
// 435 bettersort
// FontsTable() constructor: build the screen contents of the table// 436 
@SuppressWarnings("BooleanConstructorCall")// 437 checkboxes
public FontsTable() {// 438 table
public FontsTable(StringMap categories) {// 439 table&readcats
    catsMap = categories;// 440 table&readcats
    setFillsViewportHeight(true);// 441 table
    setCellSelectionEnabled(true);// 442 table
    setGridColor(new Color(200, 240, 255));// 443 table
    getTableHeader().setBackground(normalHeaderBkgd);// 444 table
    setRowHeight(INITIALFONTSIZE + ADDFORROWHEIGHT);// 445 table
    setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);// 446 table
// 447 comments
    // values to create the columns// 448 table
    String[] styleColumn // STYLECOL// 449 styles
        = new String[fontList.length];// 450 styles
    String[] categoryColumn // CATCOL// 451 cats
    = new String[fontList.length];// 452 cats
    final String[] FontColumn // FONTCOL// 453 fonts
    = new String[fontList.length];// 454 fonts
    Boolean[] checkColumn // CHECKCOL// 455 checkboxes
    = new Boolean[fontList.length];// 456 checkboxes
    JLabel[] sampleColumn // SAMPLECOL// 457 samples
    = new JLabel[fontList.length];// 458 samples
    SampleFont[] sampleColumn // SAMPLECOL// 459 fontedsamples
    = new SampleFont[fontList.length];// 460 fontedsamples
// 461 table
    for (int inx = 0; inx < fontList.length; inx++) {// 462 table
String fontname = fontList[inx].getFontName();// 463 table
String stylStr = "";// 464 styles
String catStr = null;// 465 cats
if (catsMap != null)// 466 readcats
                    catStr = catsMap.get(fontname);// 467 readcats
if (catStr == null) {// 468 readcats
    stylStr = (fontname.contains("Bold") ? "B" : "")// 469 styles
    + (fontname.contains("Italic") ? "I" : "");// 470 styles
    catStr = "";// 471 cats
}// 472 readcats
else {// 473 readcats&styles
    // for FontViewer, a StringMap value is style;category// 474 readcats&styles
    //   where style is one of "", "B", "I" or "BI"// 475 readcats&styles
    // extract style code  from front of catStr// 476 readcats&styles
    int semiloc = catStr.indexOf(";");// 477 readcats&styles
    if (semiloc >= 0) {// 478 readcats&styles
stylStr = catStr.substring(0,semiloc).trim();// 479 readcats&styles
catStr = catStr.substring(semiloc+1).trim();// 480 readcats&styles
    }// 481 readcats&styles
}// 482 readcats&styles
styleColumn[inx] = stylStr; //STYLECOL// 483 styles
categoryColumn[inx] = catStr; // CATCOL// 484 cats
FontColumn[inx] = fontname; // FONTCOL// 485 fonts
checkColumn[inx] = new Boolean(false); // CHECKCOL// 486 checkboxes
sampleColumn[inx] = new JLabel(currentSample); // SAMPLECOL// 487 samples
sampleColumn[inx] = new SampleFont(fontList[inx]); // SAMPLECOL// 488 samples&fontedsamples
    }// 489 table
// 490 table
// FontsTable() - configure the TableModel with the four columns// 491 
    // Configure the columns: data, widths, renderers, and editors// 492 table
    //     by setting limits on the widths of the first three columns,// 493 table
    //      all excess space is allocated to the sample text column// 494 table
// 495 table
    // we will create our own TableColumns// 496 table
    setAutoCreateColumnsFromModel(false);// 497 table
    // tell this JTable about the model// 498 table
    setModel(tableData);// 499 table
// 500 table
    // STYLECOL - B for bold and I for italic// 501 styles
    styleColIndex = appendColumn("BI", styleColumn,// 502 styles
    32,32,32, new FTStringRenderer(), null);// 503 styles
    // CATCOL - the categories column// 504 cats
    DefaultCellEditor catEditor// 505 celled
    = (DefaultCellEditor)getDefaultEditor(String.class);// 506 celled
    {catEditor.getComponent().setFont(labelFont);// 507 celled
     // setting a border has no effect// 508 celled
     ((JComponent)catEditor.getComponent()).setForeground(Color.RED);}// 509 celled
    catColIndex = appendColumn("Category", categoryColumn,// 510 cats
    25, 75, 150, new FTStringRenderer(), null);// 511 cats
    25, 75, 150, new FTStringRenderer(), catEditor);// 512 cats&celled
// 513 cats
    // FONTCOL - column of font names// 514 fonts
    fontColIndex = appendColumn("Font Name", FontColumn,// 515 fonts
    75, 200, 300, new FTStringRenderer(), null);// 516 fonts
// 517 fonts
    // CHECKCOL - a checkbox to select fonts// 518 checkboxes
    chkColIndex = appendColumn("Ck", checkColumn,// 519 checkboxes
    26, 26, 26, getDefaultRenderer(Boolean.class),// 520 checkboxes
    getDefaultEditor(Boolean.class));// 521 checkboxes
// 522 checkboxes
    //SAMPLECOL - sample text in this row's font// 523 samples
    sampleColIndex = appendColumn("Sample written in named font",// 524 samples
    sampleColumn, 75, 300, 10000,// 525 samples
    new FTJLabelRenderer(), null);// 526 samples
    new FTFontRenderer(), null);// 527 fontedsamples
// 528 samples
// end FontsTable() - configure the TableModel with the four columns// 529 
    // set the tooltip to be the same for all headers// 530 sort
    getTableHeader().setToolTipText(// 531 sort
    "Click checkboxes under Ck to compare fonts.  " +// 532 sort&checkboxes
    "Click a header to sort on its column.");// 533 sort
// 534 sort
    // set a renderer for column headings// 535 bettersort
    getTableHeader().setDefaultRenderer(new FTHeaderRenderer());// 536 bettersort
// 537 bettersort
    // enable sorting the table by column data// 538 sort|simplesort
    setAutoCreateRowSorter(true);// 539 simplesort
    setRowSorter(new FTRowSorter(tableData));// 540 sort
// 541 sort
    // listen for changes to the table// 542 mapchanged
    tableData.addTableModelListener(// 543 mapchanged
    new javax.swing.event.TableModelListener() {// 544 mapchanged
public void tableChanged(// 545 mapchanged
javax.swing.event.TableModelEvent e) {// 546 mapchanged
    if ((e.getColumn() == catColIndex// 547 mapchanged
    || e.getColumn() == styleColIndex// 548 mapchanged&styles
    ) && e.getType()// 549 mapchanged
== javax.swing.event.TableModelEvent.UPDATE)// 550 mapchanged
catsMap.mapChanged();// 551 mapchanged
}// 552 mapchanged
    });// 553 mapchanged
// 554 mapchanged
// FontsTable() pasteCatAction : implement paste for a cell not being edited// 555 
    // When a cell is clicked on, it is editable and ctl-V works fine// 556 comments
    // but if cell is selected with arrow keys, ctl-V does nothing// 557 comments
    // we fix that here by defining pasteCatAction// 558 comments
    // pasteCatAction - for ctl-V after typing arrow keys// 559 ctlv
    Action pasteCatAction =new FTPasteAction();// 560 ctlv
    String pasteCatKey = "pasteCat";// 561 ctlv
    KeyStroke ctlV = KeyStroke.getKeyStroke(KeyEvent.VK_V,// 562 ctlv
    ActionEvent.CTRL_MASK);// 563 ctlv
// 564 ctlv
    getInputMap().put(ctlV, pasteCatKey);// 565 ctlv
    getActionMap().put(pasteCatKey, pasteCatAction);// 566 ctlv
// end FontsTable() pasteCatAction : implement paste for a cell not being edited// 567 
    addKeyListener(new KeyAdapter() {// 568 keystyles
@Override// 569 keystyles
public void keyPressed(KeyEvent e) {// 570 keystyles
    int row = getSelectedRow();// 571 keystyles
    if (row == -1) return;// 572 keystyles
    row = convertRowIndexToModel(row);// 573 keystyles
    int col = convertColumnIndexToModel(getSelectedColumn());// 574 keystyles
    if (col == catColIndex) return;// 575 keystyles&cats
    String s = (String)getModel().getValueAt(row, styleColIndex);// 576 keystyles
    int slen = s.length();// 577 keystyles
    // s is exactly one of   ""   "B"   "I"   "BI"// 578 keystyles
    switch (e.getKeyCode()) {// 579 keystyles
    case KeyEvent.VK_B:// 580 keystyles
if (slen > 0 && s.startsWith("B"))// 581 keystyles
    s = s.substring(1);// 582 keystyles
else s = "B" + s;// 583 keystyles
break;// 584 keystyles
    case KeyEvent.VK_I:// 585 keystyles
if (slen > 0 && s.endsWith("I"))// 586 keystyles
    s = s.substring(0, slen-1);// 587 keystyles
else s += "I";// 588 keystyles
break;// 589 keystyles
    case KeyEvent.VK_BACK_SPACE:// 590 keystyles
if (slen > 0) s = s.substring(0, slen-1);// 591 keystyles
break;// 592 keystyles
    case KeyEvent.VK_DELETE:// 593 keystyles
if (slen > 0) s = s.substring(1);// 594 keystyles
break;// 595 keystyles
    default: return;// 596 keystyles
    }// 597 keystyles
    // now s has the new value// 598 keystyles
    getModel().setValueAt(s, row, styleColIndex);// 599 keystyles
    e.consume();// 600 keystyles
}// 601 keystyles
    });// 602 keystyles
} // end FontsTable constructor// 603 table
// 604 table
// end FontsTable() constructor: build the screen contents of the table// 605 
// FontsTable methods// 606 
// 607 table&comments
// 608 table&comments
/////////////////////////////////////////////// 609 table&comments
// FontsTable methods// 610 table&comments
// 611 table&comments
/**// 612 table
* Append a column to the TableModel and the TableColumnModel.// 613 table
* Convenience method for building the table.// 614 table
*/// 615 table
final int appendColumn(String name, Object[]data, int minW,// 616 table
    int prefW, int maxW,// 617 table
    TableCellRenderer render, TableCellEditor edit) {// 618 table
    int index = tableData.getColumnCount();// 619 table
    tableData.addColumn(name, data);// 620 table
    TableColumn col = new TableColumn(index, prefW, render, edit);// 621 table
    col.setMinWidth(minW);// 622 table
    col.setMaxWidth(maxW);// 623 table
    addColumn(col);// 624 table
    return index;// 625 table
}// 626 table
// 627 table
// FontsTable.refontTheSamples() : when the size or style changes, revise the fonts for all samples// 628 
/**// 629 samples
* When the size or style changes, revise the fonts for all samples// 630 samples
*/// 631 samples
private void refontTheSamples() {// 632 samples
    // get current  status// 633 samples
    int selectedRow = getSelectedRow();// 634 samples
    int selectedCol = getSelectedColumn();// 635 cellfocus
    int oldRowHeight = getRowHeight();// 636 rowheight
    // record viewRect before it is overwritten by changeSelection()// 637 rowvis
    JViewport vp = scrollTable.getViewport();// 638 rowvis
    Rectangle viewRect = vp.getViewRect();     // the visible rectangle// 639 rowvis
    // fetch the parameters for new fonts// 640 samples
    int fontsize = INITIALFONTSIZE;// 641 samples
    int fontsize = sizeModel.getNumber().intValue();// 642 samples&picksize
    int styleInt = Font.PLAIN;// 643 samples
    int styleInt = styleValues[styles.getSelectedIndex()];// 644 samples&pickstyle
// 645 samples
    // replace the fonts in all samples// 646 samples
    for (int inx = 0; inx < fontList.length; inx++) {// 647 samples
JLabel lbl = (JLabel)tableData.getValueAt(inx, sampleColIndex);// 648 samples
lbl.setFont(new Font(fontList[inx].getFontName(), styleInt, fontsize));// 649 samples
Font newf = new SampleFont(fontList[inx]// 650 fontedsamples
.deriveFont(styleInt, (float)fontsize));// 651 fontedsamples
tableData.setValueAt(newf, inx, sampleColIndex);// 652 fontedsamples
    }// 653 samples
// 654 samples
    // choose a table row height appropriate to fontsize// 655 rowheight
    int newRowHeight = Math.max(INITIALFONTSIZE - 4, fontsize )// 656 rowheight
+ADDFORROWHEIGHT;// 657 rowheight
// 658 rowheight
// FontsTable.refontTheSamples() adjust view: reinstate the selection and scroll to keep same view// 659 
    // if selected cell was visible,// 660 rowvis
    // scroll so it shows closw to where it was// 661 rowvis
    // but if it was offscreen,// 662 rowvis
    // scroll to keep the top line where it was// 663 rowvis
    int toprow;// 664 rowvis
    if  (selectedRow == -1)// 665 rowvis
// no selection, retain existing top row// 666 rowvis
toprow = rowAtPoint(new Point(0, viewRect.y));// 667 rowvis
    else  {// 668 rowvis
// return the focus to the selected cell, if any// 669 cellfocus
// (JTable turns on autoscrolls, but it scrolls twice:// 670 cellfocus
// here and in setViewPosition() below)// 671 cellfocus
setAutoscrolls(false);    // momentarily block autoscrolls// 672 cellfocus&noflash
changeSelection(selectedRow, selectedCol, false, false);// 673 cellfocus
setAutoscrolls(true);  // re-enable autoscrolls// 674 cellfocus&noflash
// 675 cellfocus
Rectangle s  // the location of the selectedRow// 676 rowvis
= getCellRect(selectedRow, 0, true);// 677 rowvis
if (s.y+s.height - SLIVER < viewRect.y// 678 rowvis
|| s.y > viewRect.y+viewRect.height - SLIVER)// 679 rowvis
    // the selection was not in the view, retain existing top row// 680 rowvis
    toprow = rowAtPoint(new Point(0, viewRect.y));// 681 rowvis
else {// 682 rowvis
    // scroll selectedRow to near its old position on the screen// 683 rowvis
    int offset = s.y - viewRect.y;// 684 rowvis
    toprow = selectedRow// 685 rowvis
    - (offset-SLIVER) / (INITIALFONTSIZE+ADDFORROWHEIGHT);// 686 rowvis
    toprow = selectedRow - (offset-SLIVER) / newRowHeight;// 687 rowvis&rowheight
}// 688 rowvis
    }// 689 rowvis
    setRowHeight(newRowHeight);  // change view geometry !// 690 rowheight
    int yTop = getCellRect(toprow, 0, true).y;// 691 rowvis
    vp.setViewPosition(new Point(0, yTop));// 692 rowvis
// 693 rowvis
// end FontsTable.refontTheSamples() adjust view: reinstate the selection and scroll to keep same view// 694 
    // the focus has moved to the sizes or styles widget// 695 cellfocus
    // return the focus to the table// 696 cellfocus
    requestFocusInWindow();// 697 cellfocus
// 698 cellfocus
    repaint();     // re-render all (to get new samples displayed)// 699 samples
} // end refontTheSamples// 700 samples
// 701 samples
// end FontsTable.refontTheSamples() : when the size or style changes, revise the fonts for all samples// 702 
// FontsTable.reviseSamples() : after the sampletext has changed in the DemoText, revise all samples// 703 
/** sample text has changed, trigger a repaint of all sample cells.*/// 704 sampletext&samples
private void reviseSamples() {// 705 sampletext&samples
    for (int inx = 0; inx < fontList.length; inx++)     // cause repaint// 706 sampletext&samples
tableData.fireTableCellUpdated(inx, sampleColIndex);// 707 sampletext&samples
}// 708 sampletext&samples
// 709 sampletext&samples
// end FontsTable.reviseSamples() : after the sampletext has changed in the DemoText, revise all samples// 710 
// FontsTable.cancelEditing() : called when any open cell editor should be closed// 711 
/**// 712 celled&canceledit
* Close any open cell editor// 713 celled&canceledit
*/// 714 celled&canceledit
public void cancelEditing() {// 715 celled&canceledit
    CellEditor ed = getCellEditor();// 716 celled&canceledit
    if (ed != null)// 717 celled&canceledit
ed.stopCellEditing();// 718 celled&canceledit
}// 719 celled&canceledit
// 720 celled&canceledit
// end FontsTable.cancelEditing() : called when any open cell editor should be closed// 721 
// FontsTable.saveCats() : if categories have changed, rebuild the map and save it// 722 
/** Save the categories file.// 723 savecats
* If it has changed.// 724 mapchanged
* Reconstruct the contents of a categories list,// 725 savecats
*  from table columns CATCOL and FONTCOL.// 726 savecats
* Call cancelEditing() before saveCats() (except from timer task).// 727 savecats&canceledit
*/// 728 savecats
protected void saveCats() {// 729 savecats
    // rebuild the catsMap// 730 savecats
    for (int inx = 0; inx < tableData.getRowCount(); inx++) {// 731 savecats
String cat = (String) tableData.getValueAt(inx, catColIndex);// 732 savecats
if (cat == null) cat = "";// 733 savecats
String styl = (String)tableData.getValueAt(inx, styleColIndex);// 734 savecats&styles
if (cat.length() == 0)// 735 savecats
if (styl.length() + cat.length() == 0)// 736 savecats&styles
    continue; // not saving blank entries// 737 savecats
cat = styl + "; " + cat;// 738 savecats&styles
String lbl = fontList[inx].getFontName();// 739 savecats
catsMap.put(lbl, cat);// 740 savecats
    }// 741 savecats
    catsMap.save();// 742 savecats
}// 743 savecats
// end FontsTable.saveCats() : if categories have changed, rebuild the map and save it// 744 
// end FontsTable methods// 745 
// FontsTable - support classes// 746 
// 747 table&comments
// 748 table&comments
//////////////////////////////////////////// 749 table&comments
// Classes for  FontsTable// 750 table&comments
// 751 table&comments
// FontsTable.SampleFont -a Font subclass with the right toString() value// 752 
/**// 753 fontedsamples
* The samples column is an array of Font objects,// 754 fontedsamples
* If the user types ctl-C to copy a samples column cell,// 755 fontedsamples
* the value later pasted will be from Font.toString(). Not useful.// 756 fontedsamples
* SampleFont.toString() returns the String that actually appears// 757 fontedsamples
*/// 758 fontedsamples
class SampleFont extends Font {// 759 fontedsamples
    public SampleFont(Font f) { super(f); }// 760 fontedsamples
    @Override    public String toString()// 761 fontedsamples
{ return currentSample; }// 762 fontedsamples
}// 763 fontedsamples
// 764 fontedsamples
// end FontsTable.SampleFont -a Font subclass with the right toString() value// 765 
// FontsTable.FTStringRenderer -a Font subclass for rendering categories and fontnames// 766 
/** RFender a table cell with my chosen font and border. */// 767 table
class FTStringRenderer extends DefaultTableCellRenderer  {// 768 table
@Override// 769 table
public Component getTableCellRendererComponent(// 770 table
JTable table,// 771 table
Object value,    // the Font for this sample cell// 772 table
boolean isSelected, boolean hasFocus,// 773 table
int row, int column) {// 774 table
    setText((String)value);// 775 table
    setFont(labelFont);// 776 table
    setBorder(hasFocus ? borderBlue : cellBorder);// 777 table
    setForeground(hasFocus ? Color.BLUE : Color.BLACK);// 778 table
    return this;// 779 table
}// 780 table
    }// 781 table
// 782 table
// end FontsTable.FTStringRenderer -a Font subclass for rendering categories and fontnames// 783 
// FontsTable.FTFontRenderer - render the sample text; the data is actually the Font object// 784 
// /** render the samples column;// 785 samples
// * show the sampletext in the row's named Font. */// 786 samples
class FTJLabelRenderer extends DefaultTableCellRenderer  {// 787 samples
class FTFontRenderer extends DefaultTableCellRenderer  {// 788 fontedsamples
@Override// 789 samples
public Component getTableCellRendererComponent(// 790 samples
JTable table,// 791 samples
Object value,    // the Font for this sample cell// 792 samples
boolean isSelected, boolean hasFocus,// 793 samples
int row, int column) { // these parameters are ignored// 794 samples
    JLabel renderer = (JLabel)value;// 795 samples
    JLabel renderer = this;  // (DefaultTableCellRenderer extends JLabel)// 796 fontedsamples
    if (scrollTable.getVerticalScrollBar().getValueIsAdjusting()) {// 797 samples&fastscroll
renderer.setText("");     // defer rendering to accelerate scrolling// 798 samples&fastscroll
renderer.setBorder(cellBorder);// 799 samples&fastscroll
    }// 800 samples&fastscroll
    else {// 801 samples&fastscroll
renderer.setText(currentSample);// 802 samples
renderer.setFont((Font)value);// 803 fontedsamples
renderer.setBorder(hasFocus ? borderBlue : cellBorder);// 804 samples
renderer.setForeground(hasFocus ? Color.BLUE : Color.BLACK);// 805 samples
    }// 806 samples&fastscroll
    return renderer;// 807 samples
}// 808 samples
    }// 809 samples
// end FontsTable.FTFontRenderer - render the sample text; the data is actually the Font object// 810 
// FontsTable.FTHeaderRenderer - render the column headers idiosyncratically// 811 
/** Render column headings.// 812 bettersort
* Omit the "sorting arrow"; we always sort ascending// 813 bettersort
* Color the header background during the sort// 814 bettersort
*/// 815 bettersort
class FTHeaderRenderer extends DefaultTableCellRenderer {// 816 bettersort
@Override// 817 bettersort
public Component getTableCellRendererComponent(// 818 bettersort
JTable table, Object value, boolean isSelected,// 819 bettersort
boolean hasFocus, int row, int column) {// 820 bettersort
    int modelCol = convertColumnIndexToModel(column);// 821 bettersort
    setText(" "+(String)value);// 822 bettersort
    setOpaque(true);// 823 bettersort
    setFont(headerFont);// 824 bettersort
    setBackground(nowSorting==modelCol// 825 bettersort
? sortingHeaderBkgd : normalHeaderBkgd);// 826 bettersort
    setForeground(Color.BLUE);// 827 bettersort
    if (modelCol == styleColIndex)// 828 bettersort&styles
    setHorizontalAlignment(JLabel.CENTER);// 829 bettersort&styles
    return this;// 830 bettersort
}// 831 bettersort
/** the header looks better with a bottom line */// 832 bettersort
    @Override// 833 bettersort
protected void paintBorder(Graphics g) {// 834 bettersort
    g.drawLine(0, getHeight()-1, getWidth(), getHeight()-1);// 835 bettersort
}// 836 bettersort
    }// 837 bettersort
// 838 bettersort
// end FontsTable.FTHeaderRenderer - render the column headers idiosyncratically// 839 
// FontsTable.FTCorner - draw an empty corner// 840 
/** Draw an empty corner for the header/scrollbar intersection.// 841 scrollcorner
same color and border as FTHeaderRenderer */// 842 scrollcorner
class FTCorner extends JPanel {// 843 scrollcorner
    @Override// 844 scrollcorner
    public void addNotify() {// 845 scrollcorner
super.addNotify();// 846 scrollcorner
setOpaque(true);// 847 scrollcorner
setBackground(normalHeaderBkgd);// 848 scrollcorner
    }// 849 scrollcorner
    @Override// 850 scrollcorner
    protected void paintBorder(Graphics g) {// 851 scrollcorner
g.drawLine(0, getHeight()-1, getWidth(), getHeight()-1);// 852 scrollcorner
    }// 853 scrollcorner
} // end FTCorner// 854 scrollcorner
// 855 scrollcorner
// end FontsTable.FTCorner - draw an empty corner// 856 
// FontsTable.FTChangeListener- listen to scroll bar changes to control repainting the samples// 857 
/** listen to changes in the vertical scroll bar.// 858 samples&fastscroll
* When it has stopped moving it sends a final change// 859 samples&fastscroll
* with valueIsAdjusting false. We have not been rendering// 860 samples&fastscroll
* the samples column while scrolling, so we render it now.// 861 samples&fastscroll
*/// 862 samples&fastscroll
class FTChangeListener// 863 samples&fastscroll
    implements javax.swing.event.ChangeListener {// 864 samples&fastscroll
    boolean moving = false;// 865 samples&fasterscroll
    public void stateChanged(javax.swing.event.ChangeEvent e) {// 866 samples&fastscroll
if (scrollTable.getVerticalScrollBar().getValueIsAdjusting()) {// 867 samples&fasterscroll
    moving = true;// 868 samples&fasterscroll
}// 869 samples&fasterscroll
else {// 870 samples&fasterscroll
    if (moving  && FontViewer.this.isDoubleBuffered()) {// 871 samples&fasterscroll
// to avoid the long wait after scrolling,// 872 samples&fasterscroll
// turn off double buffering while redrawing the samples// 873 samples&fasterscroll
moving = false;// 874 samples&fasterscroll
FontViewer.this.setDoubleBuffered(false);// 875 samples&fasterscroll
FontViewer.this.getRootPane().setDoubleBuffered(false);// 876 samples&fasterscroll
needToRestoreDoubleBuffering = true;// 877 samples&fasterscroll
    }// 878 samples&fasterscroll
    // scroll movement has stopped// 879 samples&fastscroll
    // must repaint all visible elements in SAMPLE column// 880 samples&fastscroll
    JViewport vp = scrollTable.getViewport();// 881 samples&fastscroll
    Rectangle viewRect = vp.getViewRect();// 882 samples&fastscroll
    int sampCol = mainTable// 883 samples&fastscroll
.convertColumnIndexToView(sampleColIndex);// 884 samples&fastscroll
    Rectangle cr = mainTable.getCellRect(0, sampCol, true);// 885 samples&fastscroll
    scrollTable.repaint(0, cr.x, 0,// 886 samples&fastscroll
cr.width, viewRect.height + cr.height);// 887 samples&fastscroll
}// 888 samples&fasterscroll
    }// 889 samples&fastscroll
}// 890 samples&fastscroll
// 891 samples&fastscroll
// end FontsTable.FTChangeListener- listen to scroll bar changes to control repainting the samples// 892 
// FontsTable.FTPasteAction - Action for pasting into category cells// 893 
/** An Action for pasting values into the category cells */// 894 ctlv
class FTPasteAction extends AbstractAction {// 895 ctlv
@Override// 896 ctlv
public void actionPerformed(ActionEvent e) {// 897 ctlv
    // be sure focused cell is in CATCOL// 898 ctlv
    ListSelectionModel selmod// 899 ctlv
    = getColumnModel().getSelectionModel();// 900 ctlv
    int viewcol = selmod.getLeadSelectionIndex();// 901 ctlv
    int modcol = convertColumnIndexToModel(viewcol);// 902 ctlv
    if (catColIndex != modcol)// 903 ctlv
return; //  lead cell is not in CATCOL// 904 ctlv
// 905 ctlv
    // copy the cutbuffer into sink, a temp// 906 ctlv
    sink.setText("");// 907 ctlv
    pasteSinkAction.actionPerformed(// 908 ctlv
    new ActionEvent(sink, TransferHandler.COPY, ""));// 909 ctlv
// 910 ctlv
    // copy from sink to the label// 911 ctlv
    String text = sink.getText();// 912 ctlv
    int viewrow = getSelectionModel().getLeadSelectionIndex();// 913 ctlv
    int modrow = convertRowIndexToModel(viewrow);// 914 ctlv
    setValueAt(text, viewrow, viewcol);// 915 ctlv
    repaint(getCellRect(viewrow, viewcol, true));// 916 ctlv
}// 917 ctlv
    }// 918 ctlv
// end FontsTable.FTPasteAction - Action for pasting into category cells// 919 
// FontsTable.FTRowSorter - sort rows// 920 
/**// 921 sort
* Specialized sorting routines. Strings in ascending order// 922 sort
* and checkboxes sort the checked boxes first.// 923 sort
*/// 924 sort
        private class FTRowSorter// 925 sort
extends TableRowSorter<DefaultTableModel>{// 926 sort
    FTRowSorter(DefaultTableModel tmodel) {// 927 sort
super(tmodel);// 928 sort
    }// 929 sort
    /** Comparators// 930 sort
     * @param column which column// 931 sort
     * @return appropriate Comparator// 932 sort
     */// 933 sort
    @Override// 934 sort
    public Comparator getComparator(int column) {// 935 sort
if (column == chkColIndex)// 936 sort&checkboxes
    return new Comparator<Boolean>() {// 937 sort&checkboxes
@Override// 938 sort&checkboxes
public int compare(Boolean o1, Boolean o2) {// 939 sort&checkboxes
    // reverse compare to favor checked boxes// 940 sort&checkboxes
    return o2.compareTo(o1);// 941 sort&checkboxes
}// 942 sort&checkboxes
    };// 943 sort&checkboxes
if (column == sampleColIndex)// 944 sort&samples
    return null;// 945 sort&samples
return new Comparator<String>() {// 946 sort
    @Override// 947 sort
    public int compare(String o1, String o2) {// 948 sort
return o1.compareToIgnoreCase(o2);// 949 sort
    }// 950 sort
};// 951 sort
    }// 952 sort
    @Override// 953 sort
    protected boolean useToString(int column) { return false; }// 954 sort
    @Override// 955 sort
    public boolean isSortable(int column) {// 956 sort
return true;// 957 sort
return column != sampleColIndex;// 958 sort&samples
    }// 959 sort
// 960 sort
// FontsTable.FTRowSorter.setSortKeys - call proper low level routines to choose sort order// 961 
    /** Normal sort has two problems:// 962 bettersort
    // a) if the table has been sorted on a column.// 963 bettersort
    //     reclicking the header does not sort again// 964 bettersort
    // b) each sort is in the opposite direction,// 965 bettersort
    //      this is not useful for fonts, so we sort always ascending// 966 bettersort
    // this override of setSortKeys fixes both problems// 967 bettersort
     */// 968 bettersort
    @Override// 969 sort
    public void setSortKeys(// 970 sort
java.util.List<? extends SortKey>keys) {// 971 sort
cancelEditing();   // save any in-progress category edit// 972 sort&canceledit
super.setSortKeys(keys);// 973 sort
java.util.List<SortKey> copy// 974 bettersort
= new ArrayList<SortKey>(keys.size());// 975 bettersort
for (SortKey k : keys)// 976 bettersort
    copy.add(new SortKey(k.getColumn(),// 977 bettersort
SortOrder.ASCENDING));// 978 bettersort
// during the sort, the column header changes color// 979 bettersort
// the column number in the sortkey is per the model// 980 bettersort
nowSorting = copy.get(0).getColumn();// 981 bettersort
FontsTable.this.getTableHeader()// 982 bettersort
.paintImmediately(0,0,99999,99999);// 983 bettersort
// 984 bettersort
java.util.List<SortKey>newSortKeys// 985 bettersort
= Collections.unmodifiableList(copy);// 986 bettersort
if (getSortKeys().equals(newSortKeys))// 987 bettersort
    allRowsChanged();     // force a sort// 988 bettersort
else// 989 bettersort
    super.setSortKeys(copy); // this does the actual sort// 990 bettersort
// 991 bettersort
    javax.swing.SwingUtilities.invokeLater(new Runnable() {// 992 bettersort
@Override// 993 bettersort
public void run() {// 994 bettersort
nowSorting = -1; // turn off header color// 995 bettersort
FontsTable.this.getTableHeader()// 996 bettersort
    .paintImmediately(0,0,99999,99999);// 997 bettersort
}// 998 bettersort
    });// 999 bettersort
    }  // end setSortKeys// 1000 sort
} // end FTPRowSorter// 1001 sort
// end FontsTable.FTRowSorter.setSortKeys - call proper low level routines to choose sort order// 1002 
// end FontsTable.FTRowSorter - sort rows// 1003 
// end FontsTable - support classes// 1004 
    } // end class FontsTable// 1005 table
// 1006 table
// end FontsTable - class : the central JTable for the application// 1007 
// StringMap - class : a HashMap mapping from fontname to category// 1008 
// 1009 stringmap
// 1010 stringmap
    ///////////////////////////////////////// 1011 stringmap
    //  String Map// 1012 stringmap
// 1013 stringmap
    /** Maintain a map from strings to strings.// 1014 stringmap
     *  The file format has key and value, separated with a colon.// 1015 stringmap
     *  Keys and values are each trim()ed to remove outer whitespace.// 1016 stringmap
     *  In keys, colons are converted to question marks..// 1017 stringmap
     */// 1018 stringmap
    static public class StringMap extends HashMap<String, String> {// 1019 stringmap
String catSrc = null;   // where to get the categories file// 1020 readcats
boolean mutable = true;   // FVApplet wil set this false// 1021 savecats
String catDest = null;  // file to save revised categories// 1022 savecats
boolean changed = false;    // set to true by mapChanged()// 1023 savecats
// and back to false by save()// 1024 savecats
// 1025 readcats
StringMap() {}// 1026 stringmap
/** Create a StringMap with a given source location.// 1027 readcats
* @param name Name of the data source.// 1028 readcats
* May be null to use the default map.// 1029 readcats
*/// 1030 readcats
public StringMap(String name) {// 1031 readcats
    catSrc = name;// 1032 readcats
        }// 1033 readcats
// 1034 readcats
/** called by FVApplet to prevent trying to write to disk */// 1035 savecats
void readOnly() { mutable = false; }// 1036 savecats
// 1037 savecats
/**// 1038 mapchanged
* The category map has changed (for the first time).// 1039 mapchanged
* Determine if and where to save the result.  If necessary// 1040 mapchanged
* and appropriate, prompt the user for a save location// 1041 mapchanged
*/// 1042 mapchanged
void mapChanged() {// 1043 mapchanged
    if (changed) return;// 1044 mapchanged
    changed = true;// 1045 mapchanged
    if (catDest != null)// 1046 mapchanged
return;   // catSrc is a file; save will replace that file// 1047 mapchanged
    String msg = (mutable)// 1048 mapchanged
? "Category changes will not be saved until\n"// 1049 mapchanged
+ "a file is picked via \"Save As\" in the File menu."// 1050 mapchanged
: "FontViewer cannot save category\n"// 1051 mapchanged
+ "changes when run as an applet";// 1052 mapchanged
    JOptionPane.showMessageDialog(null, msg,// 1053 mapchanged
"Warning", JOptionPane.WARNING_MESSAGE);// 1054 mapchanged
}// 1055 mapchanged
// 1056 mapchanged
void promptForCatDest() {// 1057 filechooser
    // prompt for save file location// 1058 filechooser
    JFileChooser chooser = new JFileChooser();// 1059 filechooser
    chooser.setDialogTitle("Choose a file for the categories list");// 1060 filechooser
    chooser.setSelectedFile(new File("categories.txt"));// 1061 filechooser
    chooser.setApproveButtonText("Save");// 1062 filechooser
    chooser.setApproveButtonToolTipText(// 1063 filechooser
    "Choose now a file to later<br>"// 1064 filechooser
    + "store a revised categories.txt");// 1065 filechooser
    if (chooser.showOpenDialog(null)// 1066 filechooser
== JFileChooser.APPROVE_OPTION) {// 1067 filechooser
catDest = chooser.getSelectedFile().getAbsolutePath();// 1068 filechooser
mutable = true;// 1069 filechooser
    }// 1070 filechooser
}// 1071 filechooser
// 1072 filechooser
void errorAlert(String msg) {// 1073 readcats
JOptionPane.showMessageDialog(null, msg,// 1074 readcats
"Categories file error", JOptionPane.ERROR_MESSAGE);// 1075 readcats
catDest = null;// 1076 savecats
}// 1077 readcats
// 1078 readcats
// StringMap.getReader() : parses category file/URL name and opens the stream// 1079 
/**// 1080 readcats
* Parse the src value and open a Reader on the data describes.// 1081 readcats
* Possible syntaxes:// 1082 readcats
* resource:name - look for name as a file among the class files// 1083 readcats
*     the default is resource:categories.txt// 1084 readcats
         * data:,xxxxx ... - category pairs are embedded in src itself// 1085 readcats
         *         xxxxx is a sequence of lines, each having// 1086 readcats
* fontname,':', category, and a newline// 1087 readcats
* URI - scheme://.... - open the URI// 1088 readcats
* else - assume file name; open the file// 1089 readcats
* If src fails to open, an IOException is thrown.// 1090 readcats
* catDest is set if the data comes from a file.// 1091 savecats
* @param src The location to read from, as noted above// 1092 readcats
* @return an InputStream for reading the category file// 1093 readcats
* @throws IOException if an error occurs in opening the src// 1094 readcats
*/// 1095 readcats
Reader getReader(String src) throws IOException {// 1096 readcats
    // To match URI schemes and not Windows drive letter,// 1097 readcats
    // the scheme name is expected to be at least two letters.// 1098 readcats
    Pattern uripat = Pattern.compile(// 1099 readcats
"\\A"     // start of string// 1100 readcats
+ "([a-zA-Z][a-zA-Z]+)" // scheme name// 1101 readcats
+ "\\:("  // "colon" and start scheme-specific  group 2// 1102 readcats
+ "([^,]*,)?" // optional group 3:  data: stream controls// 1103 readcats
+ "(.*)" // group 4, the data area of a "data:" URI// 1104 readcats
+ ")\\Z", // end group 2, end of string// 1105 readcats
Pattern.DOTALL | Pattern.MULTILINE);// 1106 readcats
    java.util.regex.Matcher urimat = uripat.matcher(src);// 1107 readcats
    if (urimat.matches()) {// 1108 readcats
String scheme = urimat.group(1);// 1109 readcats
if (scheme.equalsIgnoreCase("resource")) {// 1110 readcats
    InputStream cis = FontViewer.class// 1111 readcats
    .getResourceAsStream(urimat.group(2));// 1112 readcats
    return new BufferedReader(new InputStreamReader(cis));// 1113 readcats
}// 1114 readcats
else if(scheme.equalsIgnoreCase("data"))// 1115 readcats
    return new StringReader(urimat.group(4));// 1116 readcats
else// 1117 readcats
    return new InputStreamReader(// 1118 readcats
new java.net.URL(src).openStream());// 1119 readcats
    }// 1120 readcats
    else {// 1121 readcats
Reader rdr = new FileReader(catSrc);// 1122 readcats
catDest = catSrc;   // created FileReader, so catsrc exists// 1123 savecats
return rdr;// 1124 readcats
    }// 1125 readcats
}// 1126 readcats
// 1127 readcats
// endStringMap.getReader() : parses category file/URL name and opens the stream// 1128 
// StringMap.read() : reads from a BufferedReader to populate the StringMap// 1129 
public void read()  {// 1130 readcats
    if (catSrc != null) {// 1131 readcats
    if (readFromSource(catSrc))// 1132 readcats
return;// 1133 readcats
    }// 1134 readcats
    // catSrc failed, try the default// 1135 readcats
    catSrc = catsDefaultFile;// 1136 readcats
    readFromSource(catSrc);// 1137 readcats
}// 1138 readcats
// 1139 readcats
public boolean readFromSource(String src) {// 1140 readcats
    BufferedReader brdr = null;// 1141 readcats
    try {// 1142 readcats
brdr = new BufferedReader(getReader(src));// 1143 readcats
// read from brdr// 1144 readcats
String line;// 1145 readcats
while (true) {// 1146 readcats
    line = brdr.readLine();// 1147 readcats
    if (line == null) break;// 1148 readcats
    int colx = line.indexOf(':');// 1149 readcats
    if (colx < 0) continue;// 1150 readcats
    put(line.substring(0, colx).trim(), //  key before the colon// 1151 readcats
    line.substring(colx + 1).trim());   //  and value after it// 1152 readcats
}// 1153 readcats
    }// 1154 readcats
    catch (IOException ex) {// 1155 readcats
errorAlert("Failed reading categories from " + src);// 1156 readcats
catSrc = null;// 1157 readcats
catDest = null;// 1158 savecats
return false;// 1159 readcats
    }// 1160 readcats
    finally {// 1161 readcats
if (brdr != null) try { brdr.close(); } catch (IOException ex){}// 1162 readcats
    }// 1163 readcats
    return true;// 1164 readcats
}// 1165 readcats
// 1166 readcats
// end StringMap.read() : reads from a BufferedReader to populate the StringMap// 1167 
// StringMap.getWriter() : opens the inputfile for revision, or prompts user for file// 1168 
/**// 1169 savecats
* Open an output writer to save the categories file.// 1170 savecats
* catDest has already been checked as non-null// 1171 savecats
* @return The output stream, or null if catDest is not valid// 1172 savecats
*/// 1173 savecats
Writer getWriter() {// 1174 savecats
    Writer os = null;// 1175 savecats
    try {// 1176 savecats
                os = new FileWriter(catDest);// 1177 savecats
    } catch(Exception ex) {// 1178 savecats
errorAlert("Cannot write to " + catDest);// 1179 savecats
catDest = null;// 1180 savecats
    }   // if error, os remains null;// 1181 savecats
    return os;  // may be null for error// 1182 savecats
  }// 1183 savecats
// 1184 savecats
// endStringMap.getWriter() : opens the inputfile for revision, or prompts user for file// 1185 
// StringMap.save() : writes the StringMap back to a file or stdout// 1186 
/**// 1187 savecats
* Save the map to the place where it came from.// 1188 savecats
*/// 1189 savecats
public void save()  {// 1190 savecats
    if (catDest == null)// 1191 savecats
return;// 1192 savecats
    BufferedWriter bwtr = null;// 1193 savecats
    int n = 0;// 1194 savecats
    try {// 1195 savecats
bwtr = new BufferedWriter(getWriter());// 1196 savecats
n = saveToWriter(bwtr);// 1197 savecats
    } catch (Exception ex) {// 1198 savecats
errorAlert("Failed saving font categories to "// 1199 savecats
+ catDest + ": " + ex.getLocalizedMessage());// 1200 savecats
    }// 1201 savecats
    finally {// 1202 savecats
try {// 1203 savecats
    if (bwtr != null) {// 1204 savecats
changed = false;// 1205 savecats
bwtr.close();// 1206 savecats
    }// 1207 savecats
    System.out.println("Saved " + n// 1208 savecats
    + " font categories data to " + catDest);// 1209 savecats
} catch(IOException ex) {}// 1210 savecats
    }// 1211 savecats
}// 1212 savecats
// 1213 savecats
/**// 1214 savecats
* Save the StringMap data to a given stream.// 1215 savecats
* Only Entries which map to non-empty strings are saved.// 1216 savecats
* @param bwr Where to send the StringMap entries.// 1217 savecats
* @return The number of lines written to bwr..// 1218 savecats
* @throws IOException// 1219 savecats
*/// 1220 savecats
public int saveToWriter(BufferedWriter bwr) throws IOException {// 1221 savecats
    int n = 0;// 1222 savecats
    ArrayList<java.util.Map.Entry<String, String>> entries// 1223 savecats
    = new ArrayList<java.util.Map.Entry<String, String>>// 1224 savecats
(entrySet());// 1225 savecats
    Collections.sort(entries,// 1226 sortmap
    new Comparator<java.util.Map.Entry<String, String>>() {// 1227 sortmap
@Override// 1228 sortmap
public int compare(java.util.Map.Entry<String, String> o1,// 1229 sortmap
java.util.Map.Entry<String, String> o2) {// 1230 sortmap
    return o1.getKey().compareToIgnoreCase(o2.getKey());// 1231 sortmap
}// 1232 sortmap
    });// 1233 sortmap
    for (java.util.Map.Entry<String, String> e : entries) {// 1234 savecats
String key = e.getKey();// 1235 savecats
String val = e.getValue();// 1236 savecats
if (val == null || val.length() == 0)// 1237 savecats
    continue;     // skip entries that map font name to blank// 1238 savecats
bwr.write(key + " : " + val);// 1239 savecats
bwr.newLine();// 1240 savecats
n++;// 1241 savecats
    }// 1242 savecats
    return n;// 1243 savecats
} // end save()// 1244 savecats
// end StringMap.save() : writes the StringMap back to a file or stdout// 1245 
    } // end class StringMap// 1246 stringmap
// 1247 stringmap
// end StringMap - class : a HashMap mapping from fontname to category// 1248 
// FontViewer.addMenuItem() common code for making menu items// 1249 
    /**// 1250 menu
     * Add a menu item to a menu, and set its listener and code.// 1251 menu
     * @param menu The destination menu or menubar.// 1252 menu
     * @param name  The name that will appear in the menu item.// 1253 menu
     * @param ear  Listener to be called when the menu item is selected.// 1254 menu
     * @param mnemonic Which key will select this menu item.// 1255 menu
     * @param cmd String that will be available to the ActionListener// 1256 menu
     * as the actionCommand.// 1257 menu
     */// 1258 menu
    static JMenuItem addMenuItem(JComponent menu, String name,// 1259 menu
ActionListener ear, int mnemonic, String cmd) {// 1260 menu
JMenuItem item = new JMenuItem(name, mnemonic);// 1261 menu
item.setActionCommand(cmd);// 1262 menu
item.addActionListener(ear);// 1263 menu
menu.add(item);// 1264 menu
return item;// 1265 menu
    }// 1266 menu
// 1267 menu
// end FontViewer.addMenuItem() common code for making menu items// 1268 
// 1269 backbone
// 1270 backbone
    ///////////////////////////////////////// 1271 backbone
    // main()// 1272 backbone
// 1273 backbone
// FontViewer.main() : the main program if FontViewer is not an applet or part of a larger application// 1274 
    /** Run this application// 1275 backbone
     * @param args  If given, the location of initial StringMap categories// 1276 backbone
     */// 1277 backbone
    public static void main(String[] args) {// 1278 backbone
// 1279 backbone
FontViewer.printFontList(System.out);// 1280 offline
// 1281 offline
// The default UI is "metal." Its design for the`sizes' JSpinner is ugly// 1282 laf
// So we use the native look and feel..// 1283 laf
try {// 1284 laf
    String className// 1285 laf
    = UIManager.getSystemLookAndFeelClassName();// 1286 laf
    UIManager.setLookAndFeel(className);// 1287 laf
} catch (Exception cnf) {}   // unlikely and not vital, ignore the error// 1288 laf
// 1289 laf
// where to read the font categories info// 1290 readcats
String categoryFile// 1291 readcats
    = (args.length > 0) ? args[0] : null;// 1292 readcats
/* sample values for categoryFile:// 1293 readcats
    = "data:,Accent:Odd Bold\nAgency FB:Sans B T\n";// 1294 readcats
    = "http://physpics.com/Java/apps/FontViewer/fontcategories.txt";// 1295 readcats
    = "g:\\fred\\java\\FontViewer\\distsrc\\fontcategories.txt"; */// 1296 readcats
// Note: many fonts in the default fontcategories.txt are in cygwin// 1297 readcats
// 1298 readcats
// create FontViewer to be tested// 1299 window
final FontViewer subject = new FontViewer();// 1300 window
final FontViewer subject = new FontViewer(new StringMap());// 1301 stringmap
final StringMap cats = new StringMap(categoryFile);// 1302 readcats
cats.read();// 1303 readcats
final FontViewer subject = new FontViewer(cats);// 1304 readcats
// 1305 window
final JFrame frame = new JFrame(" Font Viewer");// 1306 window
frame.setContentPane(subject);// 1307 window
frame.setIconImage(Toolkit.getDefaultToolkit().createImage(// 1308 laf
FontViewer.class.getResource("images/FV16.png")));// 1309 laf
// 1310 window
JMenuBar menuBar = new JMenuBar();// 1311 menu
JMenu fileMenu = new JMenu("File");// 1312 menu
JMenuItem ti;// 1313 menu
ti = addMenuItem(fileMenu, "Save Categories",// 1314 menu&savecats
subject, KeyEvent.VK_S, "S");// 1315 menu&savecats
ti.setAccelerator(// 1316 menu&savecats
KeyStroke.getKeyStroke(KeyEvent.VK_S,// 1317 menu&savecats
InputEvent.CTRL_DOWN_MASK));// 1318 menu&savecats
addMenuItem(fileMenu, "Save As ...", subject, KeyEvent.VK_A, "A");// 1319 menu&filechooser
addMenuItem(fileMenu, "Exit", subject, KeyEvent.VK_X, "X");// 1320 menu&shut
menuBar.add(fileMenu);// 1321 menu
JMenu helpMenu = new JMenu("Help");// 1322 menu
if (Desktop.isDesktopSupported() && Desktop// 1323 menu
.getDesktop().isSupported(Desktop.Action.BROWSE)) {// 1324 menu
ti = addMenuItem(helpMenu, "Help",// 1325 menu
subject, KeyEvent.VK_H, "H");// 1326 menu
ti.setAccelerator(KeyStroke// 1327 menu
.getKeyStroke(KeyEvent.VK_F1, 0));// 1328 menu
}// 1329 menu
ti = new JMenuItem(subject.sampleEditor.resetAction);// 1330 menu&resetaction
helpMenu.add(ti);// 1331 menu&resetaction
menuBar.add(helpMenu);// 1332 menu
frame.setJMenuBar(menuBar);// 1333 menu
// 1334 menu
subject.startSaving(10*60*1000);   // save categories every 10 min.// 1335 savecats&autosave
// 1336 savecats&autosave
// FontViewer.main() - window closure : save the categories file// 1337 
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 1338 window
frame.setDefaultCloseOperation(// 1339 shut
WindowConstants.DISPOSE_ON_CLOSE);// 1340 shut
frame.addWindowListener(new WindowAdapter() {// 1341 shut
    void saveAndExit() {// 1342 shut
subject.mainTable.cancelEditing();// 1343 shut&canceledit
if (cats.changed) {// 1344 shut&savecats
    if (cats.catDest == null)// 1345 shut&savecats
cats.promptForCatDest();// 1346 shut&filechooser
    subject.mainTable.saveCats();// 1347 shut&savecats
}// 1348 shut&savecats
// exit only after all processing for the current event// 1349 shut
java.awt.EventQueue.invokeLater(// 1350 shut
new Runnable() {// 1351 shut
    @Override// 1352 shut
    public void run() { System.exit(0); }// 1353 shut
});// 1354 shut
    }// 1355 shut
    @Override// 1356 shut
    public void windowClosing(WindowEvent we) {// 1357 shut
saveAndExit();// 1358 shut
    }// 1359 shut
    @Override// 1360 shut
    public void windowClosed(WindowEvent we) {// 1361 shut
saveAndExit();// 1362 shut
    }// 1363 shut
});// 1364 shut
// 1365 shut
// end FontViewer.main() - window closure : save the categories file// 1366 
// FontViewer.main().invokeLater() : launch the application// 1367 
javax.swing.SwingUtilities.invokeLater(new Runnable() {// 1368 window
    @Override// 1369 window
    public void run() {// 1370 window
frame.setPreferredSize(new Dimension(725, 800));// 1371 window
frame.pack();// 1372 window
subject.mainTable.requestFocusInWindow();// 1373 window&table
frame.setVisible(true);// 1374 window
    }// 1375 window
});// 1376 window
// end FontViewer.main().invokeLater() : launch the application// 1377 
    } // end method main()// 1378 backbone
// end FontViewer.main() : the main program if FontViewer is not an applet or part of a larger application// 1379 
}  // end class FontViewer// 1380 backbone
// end FontViewer class// 1381 
// end FontViewer.java - Display system fonts, with a sample in each// 1382 
 
Copyright © 2012 Zweibieren, All rights reserved. Feb 12, 2012 16:30 GMT Page maintained by Zweibieren
 
two steins of beer