1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2009, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.interview.wizard;
  28 
  29 import java.awt.BorderLayout;
  30 import java.awt.Dimension;
  31 import java.awt.EventQueue;
  32 import java.awt.Frame;
  33 import java.awt.Insets;
  34 import java.awt.Toolkit;
  35 import java.awt.Window;
  36 import java.awt.event.ActionEvent;
  37 import java.awt.event.ActionListener;
  38 import java.awt.event.WindowAdapter;
  39 import java.awt.event.WindowEvent;
  40 import java.io.*;
  41 import java.lang.reflect.InvocationTargetException;
  42 import java.lang.reflect.Method;
  43 import java.net.URL;
  44 import java.nio.charset.StandardCharsets;
  45 import java.util.Map;
  46 import java.util.Properties;
  47 import javax.swing.BorderFactory;
  48 import javax.swing.Box;
  49 import javax.swing.Icon;
  50 import javax.swing.ImageIcon;
  51 import javax.swing.JButton;
  52 import javax.swing.JComponent;
  53 import javax.swing.JDialog;
  54 import javax.swing.JFileChooser;
  55 import javax.swing.JFrame;
  56 import javax.swing.JMenu;
  57 import javax.swing.JMenuBar;
  58 import javax.swing.JMenuItem;
  59 import javax.swing.JOptionPane;
  60 import javax.swing.JPanel;
  61 import javax.swing.JPopupMenu;
  62 import javax.swing.JSplitPane;
  63 import javax.swing.JToggleButton;
  64 import javax.swing.JToolBar;
  65 import javax.swing.KeyStroke;
  66 import javax.swing.UIManager;
  67 import javax.swing.WindowConstants;
  68 import javax.swing.event.AncestorEvent;
  69 import javax.swing.event.AncestorListener;
  70 import javax.swing.event.PopupMenuEvent;
  71 import javax.swing.event.PopupMenuListener;
  72 import javax.swing.filechooser.FileFilter;
  73 
  74 import com.sun.interview.Interview;
  75 import com.sun.interview.Question;
  76 import com.sun.interview.WizPrint;
  77 import com.sun.javatest.tool.jthelp.HelpBroker;
  78 import com.sun.javatest.tool.jthelp.HelpSet;
  79 import com.sun.javatest.tool.jthelp.JTHelpBroker;
  80 
  81 /**
  82  * A wizard to present an {@link Interview interview} consisting of
  83  * a series of {@link Question questions}.
  84  *
  85  * <p>The tool can be started as an application itself,
  86  * by using the {@link #main main}
  87  * method. This requires that the class name of the interview
  88  * be supplied as the first argument; the class itself must be on
  89  * the tool's class path. This technique allows any interview
  90  * to be run by this tool.
  91  * <p>An alternative technique is to provide a small default main method
  92  * inside each interview, which creates an instance of the interview
  93  * and starts up a tool such as this one to run the interview.
  94  *<pre>
  95  *    import javasoft.sqe.wizard.Interview;
  96  *    import javasoft.sqe.wizard.swing.Wizard;
  97  *
  98  *    public class Demo extends Interview {
  99  *        public static void main(String[] args) {
 100  *          Demo d = new Demo();
 101  *          Wizard w = new Wizard(d);
 102  *          w.showInFrame(true);
 103  *        }
 104  *    }
 105  *</pre>
 106  */
 107 public class Wizard extends JComponent {
 108     /**
 109      * A minimal main program to invoke the wizard on a specified interview.
 110      * @param args Only one argument is accepted: the name of a class which is
 111      * a subtype of {@link Interview}.
 112      */
 113     public static void main(String[] args) {
 114         try {
 115             UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
 116 
 117             Class ic = (Class.forName(args[0], true, ClassLoader.getSystemClassLoader()));
 118             Interview i = (Interview)(ic.newInstance());
 119             Wizard w = new Wizard(i);
 120             w.showInFrame(true);
 121         }
 122         catch (Throwable e) {
 123             e.printStackTrace();
 124             System.exit(1);
 125         }
 126     }
 127 
 128     /**
 129      * Create a wizard to present an interview.
 130      * @param i The interview to be presented.
 131      */
 132     public Wizard(Interview i) {
 133         this(i, null);
 134     }
 135 
 136     /**
 137      * Create a wizard to present an interview.
 138      * @param i The interview to be presented.
 139      * @param e An array of exporters to which the interview can be exported.
 140      */
 141     public Wizard(Interview i, Exporter[] e) {
 142         interview = i;
 143         exporters = e;
 144     }
 145 
 146     /**
 147      * Open a file and load it into the interview for this wizard.
 148      * This does not affect the name of the current file.
 149      * @param f The file to be loaded.
 150      * @throws IOException if any problems occur while reading the file.
 151      * @throws Interview.Fault if the checksum is missing or incorrect in the file
 152      * @see Interview#load
 153      * @see #setFile
 154      */
 155     public void open(File f) throws Interview.Fault, IOException {
 156         try (InputStream in = new BufferedInputStream(new FileInputStream(f))) {
 157 
 158             Map<String, String> stringProps = com.sun.javatest.util.Properties.load(in);
 159             interview.load(stringProps);
 160             interview.setEdited(false);
 161             String info = stringProps.get("INFO");
 162             if (info == null) {
 163                 info = "true";
 164             }
 165             initialInfoVisible = info.equals("true");
 166         }
 167     }
 168 
 169     /**
 170      * Save the current responses to the interview's questions in a file..
 171      * @param f The file in which to save the responses.
 172      * @throws IOException if any problems occur while reading the file.
 173      * @see Interview#save
 174      */
 175     public void save(File f) throws IOException {
 176         try (OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
 177             Properties p = new Properties();
 178             if (infoPanel != null)
 179                 p.put("INFO", String.valueOf(infoPanel.isShowing()));
 180             interview.save(com.sun.javatest.util.Properties.convertToStringProps(p));
 181             interview.setEdited(false);
 182             p.save(out, "Wizard data file: " + interview.getTitle());
 183         }
 184     }
 185 
 186     /**
 187      * Get the name of the current file associated with this interview.
 188      * @return the file for the interview
 189      * @see #setFile
 190      */
 191     public File getFile() {
 192         return currFile;
 193     }
 194 
 195     /**
 196      * Set the name of the current file associated with this interview.
 197      * The file may be used as a default in open/save operations.
 198      * @param f The file to be associated with this interview.
 199      * @see #getFile
 200      * @see #setDefaultFile
 201      */
 202     public void setFile(File f) {
 203         currFile = new File(f.getAbsolutePath());
 204         if (window != null)
 205             updateTitle(window);
 206     }
 207 
 208     /**
 209      * Set the name of a default file associated with this interview.
 210      * The default file is used for the name of the current value
 211      * if the user performs a File>New operation. In addition, if the
 212      * default file is set, and the current file matches the default file,
 213      * it will not be shown in the title bar.
 214      * @param f The default file to be associated with this interview.
 215      */
 216     public void setDefaultFile(File f) {
 217         defaultFile = f;
 218         if (window != null)
 219             updateTitle(window);
 220     }
 221 
 222     /**
 223      * Set the help broker in which context sensitive help and default menu help
 224      * is displayed. If not set, a default help broker will be created.
 225      * @param helpBroker The help broker to use for context sensitive and menu help.
 226      */
 227     public void setHelpBroker(HelpBroker helpBroker) {
 228         helpHelpBroker = helpBroker;
 229     }
 230 
 231     /**
 232      * Set the help set to be used for context sensitive help and the default menu help.
 233      * If not set, the interview's help set will be used.
 234      * @param helpSet The help set to use for context sensitive and menu help.
 235      *
 236      */
 237     public void setHelpSet(HelpSet helpSet) {
 238         helpHelpSet = helpSet;
 239     }
 240 
 241     /**
 242      * Set the prefix string for the help IDs for context sensitive help and default menu help.
 243      * If not set, the default is "wizard.".
 244      * @param helpPrefix A prefix to be used for all context sentive help and menu entries.
 245      */
 246     public void setHelpSetPrefix(String helpPrefix) {
 247         helpHelpPrefix = helpPrefix;
 248     }
 249 
 250     /**
 251      * Set the help menu to be used on the wizard. If not set, the default is a menu
 252      * containing a single "Help" entry.
 253      * @param helpMenu The help menu to be used.
 254      */
 255     public void setHelpMenu(JMenu helpMenu) {
 256         this.helpMenu = helpMenu;
 257     }
 258 
 259 
 260     /**
 261      * Show the wizard in a frame centered on the screen.
 262      * @param exitOnClose Set to true if the JVM should be exited when the frame is closed.
 263      */
 264     public void showInFrame(final boolean exitOnClose) {
 265         if (window != null && !(window instanceof JFrame))
 266             throw new IllegalStateException();
 267 
 268         if (!EventQueue.isDispatchThread()) {
 269             EventQueue.invokeLater(new Runnable() {
 270                 public void run() {
 271                     showInFrame(exitOnClose);
 272                 }
 273             });
 274             return;
 275         }
 276 
 277         initGUI();
 278         okBtn.setVisible(false);
 279         cancelBtn.setVisible(false);
 280 
 281         final JFrame f = new JFrame();
 282         initMenuBar(f);
 283         updateTitle(f);
 284         f.setName("interview.wizard");
 285         f.setJMenuBar(menuBar);
 286         f.setContentPane(main);
 287         f.pack();
 288 
 289         f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
 290         f.addWindowListener(new WindowAdapter() {
 291             public void windowClosing(WindowEvent e) {
 292                 if (interview.isEdited() && !okToContinue())
 293                     return;
 294                 e.getWindow().dispose();
 295             }
 296 
 297             public void windowClosed(WindowEvent e) {
 298                 if (exitOnClose)
 299                     System.exit(0);
 300             }
 301         });
 302 
 303         Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 304         Dimension size = f.getSize();
 305         f.setLocation(screenSize.width/2 - size.width/2, screenSize.height/2 - size.height/2);
 306         f.show();
 307 
 308         window = f;
 309     }
 310 
 311     /**
 312      * Action command for the okListener for {@link #showInDialog}.
 313      */
 314     public static final String OK = "OK";
 315 
 316     /**
 317      * Show the wizard in a dialog.
 318      * @param parent The parent frame for this dialog.
 319      * @param okListener A listener to e notified when the dialog is dismissed.
 320      */
 321     public void showInDialog(final Frame parent, final ActionListener okListener) {
 322         if (window != null && !(window instanceof JDialog))
 323             throw new IllegalStateException();
 324 
 325         if (!EventQueue.isDispatchThread()) {
 326             EventQueue.invokeLater(new Runnable() {
 327                 public void run() {
 328                     showInDialog(parent, okListener);
 329                 }
 330             });
 331             return;
 332         }
 333 
 334         this.okListener = okListener;
 335 
 336         initGUI();
 337         okBtn.setVisible(true);
 338         okBtn.setEnabled(interview.isFinishable());
 339         cancelBtn.setVisible(true);
 340 
 341         final JDialog d = new JDialog(parent);
 342         initMenuBar(d);
 343         updateTitle(d);
 344         d.setJMenuBar(menuBar);
 345         d.setContentPane(main);
 346         d.pack();
 347 
 348         d.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
 349         d.addWindowListener(new WindowAdapter() {
 350             public void windowClosing(WindowEvent e) {
 351                 if (!interview.isEdited() || okToContinue())
 352                     e.getWindow().dispose();
 353             }
 354 
 355             public void windowClosed(WindowEvent e) {
 356             }
 357         });
 358 
 359         Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 360         Dimension size = d.getSize();
 361         d.setLocation(screenSize.width/2 - size.width/2, screenSize.height/2 - size.height/2);
 362         d.show();
 363 
 364         window = d;
 365     }
 366 
 367     /**
 368      * Check if this object is being displayed on the screen.
 369      * @return true if the wizard is currently being displayed,
 370      * and false otherwise.
 371      */
 372     public boolean isShowing() {
 373         return (window != null && window.isShowing());
 374     }
 375 
 376     /**
 377      * Ensure that this object is showing in front of all other windows
 378      * on the screen. If the object is not currently visible, the call
 379      * has no effect.
 380      */
 381     public void toFront() {
 382         if (window != null)
 383             window.toFront();
 384     }
 385 
 386     /**
 387      * Initialize the frame's GUI components
 388      */
 389     private void initGUI() {
 390 
 391         title = interview.getTitle();
 392         if (title == null || title.equals(""))
 393             title = i18n.getString("wizard.defaultTitle");
 394 
 395         //main = new JPanel(new BorderLayout());
 396         setLayout(new BorderLayout());
 397         main = this;
 398 
 399         questionPanel = new QuestionPanel(interview);
 400         questionPanel.setBorder(BorderFactory.createLoweredBevelBorder());
 401         pathPanel = new PathPanel(questionPanel, interview);
 402 
 403         if (interview.getHelpSet() != null)
 404             infoPanel = new InfoPanel(interview);
 405 
 406         buttonPanel = new JToolBar();
 407         buttonPanel.setFloatable(false);
 408         //buttonPanel.setBorder(BorderFactory.createRaisedBevelBorder());
 409 
 410         buttonPanel.add(Box.createHorizontalGlue());
 411         backBtn = createButton("back", "performBack", performer);
 412         buttonPanel.add(backBtn);
 413         nextBtn = createButton("next", "performNext", performer);
 414         buttonPanel.add(nextBtn);
 415         buttonPanel.addSeparator();
 416         okBtn = createButton("ok", "performOk", performer);
 417         buttonPanel.add(okBtn);
 418         cancelBtn = createButton("cancel", "performCancel", performer);
 419         buttonPanel.add(cancelBtn);
 420         if (infoPanel != null) {
 421             buttonPanel.addSeparator();
 422             infoBtn = createToggle("info", "performInfo", performer);
 423             infoBtn.setSelected(initialInfoVisible);
 424             buttonPanel.add(infoBtn);
 425         }
 426         buttonPanel.addAncestorListener(new Listener());
 427 
 428         body = new JPanel(new BorderLayout());
 429         body.add(pathPanel, BorderLayout.WEST);
 430         body.add(questionPanel, BorderLayout.CENTER);
 431         body.add(buttonPanel, BorderLayout.SOUTH);
 432 
 433         body.registerKeyboardAction(performer, "performFindNext", KeyStroke.getKeyStroke("F3"),
 434                                            JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 435 
 436         if (helpHelpPrefix == null)
 437             helpHelpPrefix = "wizard.";
 438 
 439         if (helpHelpSet == null && infoPanel != null)
 440             helpHelpSet = infoPanel.getHelpSet();
 441 
 442         if (helpHelpBroker == null && helpHelpSet != null)
 443             helpHelpBroker = new JTHelpBroker();
 444 
 445         if (helpHelpBroker != null && helpHelpSet != null)
 446             helpHelpBroker.enableHelpKey(main, helpHelpPrefix + "window.csh");
 447         if (infoPanel == null)
 448             main.add(body);
 449         else
 450             update(infoBtn.isSelected());
 451     }
 452 
 453     private void initMenuBar(Window w) {
 454         menuBar = new JMenuBar();
 455 
 456         fileMenu = createMenu("file", fileMenuData, performer);
 457         if (w instanceof JFrame) {
 458             fileMenu.addSeparator();
 459             fileMenu.add(createMenuItem("file", "exit", "performExit", performer));
 460         }
 461         else {
 462             fileMenu.addSeparator();
 463             fileMenu.add(createMenuItem("file", "close", "performCancel", performer));
 464         }
 465 
 466 
 467         if (exporters != null) {
 468             // replace the default "export log" item with a full export submenu
 469             for (int i = 0; i < fileMenu.getItemCount(); i++) {
 470                 JMenuItem mi = fileMenu.getItem(i);
 471                 if (mi != null && mi.getActionCommand().equals("performExportLog")) {
 472                     fileMenu.remove(i);
 473                     JMenu exportMenu = new ExportMenu(exporters);
 474                     exportMenu.add(createMenuItem("export", "log", "performExportLog", performer));
 475                     fileMenu.insert(exportMenu, i);
 476                     break;
 477                 }
 478             }
 479         }
 480         menuBar.add(fileMenu);
 481 
 482         JMenu searchMenu = createMenu("search", searchMenuData, performer);
 483         menuBar.add(searchMenu);
 484 
 485         if  (helpHelpBroker != null) {
 486             if (helpMenu == null)
 487                 helpMenu = createMenu("help", helpMenuData, performer);
 488             menuBar.add(helpMenu);
 489         }
 490     }
 491 
 492     private void update(boolean showInfoPanel) {
 493         Dimension bodySize = body.getSize();
 494         if (bodySize.width == 0)
 495             bodySize = body.getPreferredSize();
 496 
 497         Dimension infoSize = infoPanel.getSize();
 498         if (infoSize.width == 0)
 499             infoSize = infoPanel.getPreferredSize();
 500         // need to capture the next value before we remove everything from main
 501         boolean infoPanelIsShowing = infoPanel.isShowing();
 502 
 503         main.removeAll();
 504 
 505         if (showInfoPanel) {
 506             // body-help
 507             JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, body, infoPanel);
 508             sp.setDividerLocation(bodySize.width + 2);
 509             main.add(sp);
 510             infoPanel.setCurrentID(interview.getCurrentQuestion());
 511         }
 512         else {
 513             // body
 514             main.add(body);
 515         }
 516 
 517         if (window != null) {
 518             int divWidth = new JSplitPane().getDividerSize();
 519             Dimension winSize = window.getSize();
 520             int newWidth = winSize.width;
 521             if (showInfoPanel != infoPanelIsShowing)
 522                 newWidth += (showInfoPanel ? +1 : -1) * (infoSize.width + divWidth + 4);
 523             window.setSize(newWidth, winSize.height);
 524         }
 525 
 526         if (infoBtn.isSelected() != showInfoPanel)
 527             infoBtn.setSelected(showInfoPanel);
 528     }
 529 
 530     private void updateTitle(Window w) {
 531         String t;
 532         if (currFile == null
 533             || (defaultFile != null && currFile.equals(defaultFile)))
 534             t = title;
 535         else
 536             t = i18n.getString("wizard.titleAndFile", new Object[] {title, currFile.getPath()});
 537         if (w instanceof JFrame)
 538             ((JFrame) w).setTitle(t);
 539         else
 540             ((JDialog) w).setTitle(t);
 541     }
 542 
 543     /**
 544      * Invoke a performXXX method via reflection
 545      * @param s The name of the method to be invoked.
 546      */
 547     private void perform(String s) {
 548         try {
 549             Method m = Wizard.class.getDeclaredMethod(s, new Class[] { });
 550             m.invoke(Wizard.this, new Object[] { });
 551         }
 552         catch (IllegalAccessException ex) {
 553             System.err.println(s);
 554             ex.printStackTrace();
 555         }
 556         catch (InvocationTargetException ex) {
 557             System.err.println(s);
 558             ex.getTargetException().printStackTrace();
 559         }
 560         catch (NoSuchMethodException ex) {
 561             System.err.println(s);
 562         }
 563     }
 564 
 565     /**
 566      * Handle the "back" action
 567      */
 568     private void performBack() {
 569         try {
 570             questionPanel.saveCurrentResponse();
 571             interview.prev();
 572         }
 573         catch (Interview.Fault e) {
 574             // exception normally means no more questions
 575             // e.printStackTrace();
 576         }
 577         catch (RuntimeException e) {
 578             // typically NumberFormatError
 579             // SEE ALSO QuestionPanel.showInetAddressQuestion
 580             // which wants to throw Interview.Fault from
 581             // the value saver, but can't
 582             questionPanel.getToolkit().beep();
 583         }
 584     }
 585 
 586     /**
 587      * Handle the "cancel" action
 588      */
 589     private void performCancel() {
 590         questionPanel.saveCurrentResponse();
 591         if (interview.isEdited() && !okToContinue())
 592             return;
 593         window.dispose();
 594     }
 595 
 596     /**
 597      * Handle the "exit" action
 598      */
 599     private void performExit() {
 600         questionPanel.saveCurrentResponse();
 601         if (interview.isEdited() && !okToContinue())
 602             return;
 603         // setVisible(false);
 604         System.exit(0); // uugh
 605     }
 606 
 607     /**
 608      * Handle the "exportLog" action
 609      */
 610     private void performExportLog() {
 611         questionPanel.saveCurrentResponse();
 612         JFileChooser chooser = new JFileChooser();
 613         if (currFile != null) {
 614             //  setCurrentDirectory required
 615             chooser.setCurrentDirectory(new File(currFile.getParent()));
 616             int dot = currFile.getName().lastIndexOf(".");
 617             if (dot != -1) {
 618                 File f = new File(currFile.getName().substring(0, dot) + ".html");
 619                 chooser.setSelectedFile(f);
 620             }
 621         }
 622         else {
 623             chooser.setCurrentDirectory(getUserDir());
 624         }
 625         chooser.setFileFilter(htmlFilter);
 626         //chooser.addChoosableFileFilter(txtFilter);
 627         int action = chooser.showDialog(main, i18n.getString("wizard.exportLog"));
 628         if (action != JFileChooser.APPROVE_OPTION)
 629             return;
 630 
 631         File f = ensureExtn(chooser.getSelectedFile(), ".html");
 632         if (f.exists() && !okToOverwrite(f))
 633             return;
 634         try {
 635             Writer out = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8);
 636             WizPrint w = new WizPrint(interview, interview.getPath());
 637             w.setShowResponses(true);
 638             w.write(out);
 639         }
 640         catch (FileNotFoundException e) {
 641             JOptionPane.showMessageDialog(main,
 642                                           i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
 643                                           i18n.getString("wizard.fileNotFound.title"),
 644                                           JOptionPane.ERROR_MESSAGE);
 645         }
 646         catch (IOException e) {
 647             JOptionPane.showMessageDialog(main,
 648                                           i18n.getString("wizard.badFile.txt", e.getMessage()),
 649                                           i18n.getString("wizard.badFile.title"),
 650                                           JOptionPane.ERROR_MESSAGE);
 651         }
 652     }
 653 
 654     /**
 655      * Handle the "find" action
 656      */
 657     private void performFind() {
 658         if (searchDialog == null)
 659             searchDialog = SearchDialog.create(window, interview, helpHelpBroker, helpHelpPrefix);
 660         searchDialog.setVisible(true);
 661     }
 662 
 663     /**
 664      * Handle the "find" action
 665      */
 666     private void performFindNext() {
 667         if (searchDialog == null)
 668             searchDialog = SearchDialog.create(window, interview, helpHelpBroker, helpHelpPrefix);
 669         searchDialog.find();
 670     }
 671 
 672     /**
 673      * Handle the "help" action
 674      */
 675     private void performHelp() {
 676         helpHelpBroker.displayCurrentID(helpHelpPrefix + "intro.csh");
 677     }
 678 
 679     /**
 680      * Handle the "info" action
 681      */
 682     private void performInfo() {
 683         boolean infoOn = infoBtn.isSelected();
 684         if (infoPanel.isShowing() != infoOn) {
 685             update(infoOn);
 686             window.validate();
 687         }
 688     }
 689 
 690     /**
 691      * Handle the "new" action
 692      */
 693     private void performNew() {
 694         questionPanel.saveCurrentResponse();
 695         if (interview.isEdited() && !okToContinue())
 696             return;
 697         interview.clear();
 698         interview.setEdited(false);
 699         setFile(defaultFile);
 700     }
 701 
 702     /**
 703      * Handle the "next" action
 704      */
 705     private void performNext() {
 706         try {
 707             questionPanel.saveCurrentResponse();
 708             interview.next();
 709         }
 710         catch (Interview.Fault e) {
 711             // exception normally means no more questions
 712             // e.printStackTrace();
 713             questionPanel.getToolkit().beep();
 714         }
 715         catch (RuntimeException e) {
 716             // typically NumberFormatError
 717             questionPanel.getToolkit().beep();
 718         }
 719     }
 720 
 721     /**
 722      * Handle the "ok" action
 723      */
 724     private void performOk() {
 725         try {
 726             questionPanel.saveCurrentResponse();
 727             window.dispose();
 728             okListener.actionPerformed(new ActionEvent(this,
 729                                                        ActionEvent.ACTION_PERFORMED,
 730                                                        OK));
 731         }
 732         catch (RuntimeException e) {
 733             // typically NumberFormatError
 734             questionPanel.getToolkit().beep();
 735         }
 736     }
 737 
 738     /**
 739      * Handle the "open" action
 740      */
 741     private void performOpen() {
 742         questionPanel.saveCurrentResponse();
 743         if (interview.isEdited() && !okToContinue())
 744             return;
 745 
 746         JFileChooser chooser = new JFileChooser();
 747         // set current directory from file or user.dir
 748         if (currFile != null) {
 749             // setCurrentDirectory required
 750             chooser.setCurrentDirectory(new File(currFile.getParent()));
 751             chooser.setSelectedFile(new File(currFile.getName()));
 752         }
 753         else {
 754             chooser.setCurrentDirectory(getUserDir());
 755         }
 756         chooser.setFileFilter(jtiFilter);
 757         int action = chooser.showOpenDialog(main);
 758         if (action != JFileChooser.APPROVE_OPTION)
 759             return;
 760         File f = ensureExtn(chooser.getSelectedFile(), ".jti");
 761         try {
 762             open(f);
 763             setFile(f);
 764         }
 765         catch (Interview.Fault e) {
 766             JOptionPane.showMessageDialog(main,
 767                                           i18n.getString("wizard.badInterview.txt", e.getMessage()),
 768                                           i18n.getString("wizard.badInterview.title"),
 769                                           JOptionPane.ERROR_MESSAGE);
 770         }
 771         catch (FileNotFoundException e) {
 772             JOptionPane.showMessageDialog(main,
 773                                           i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
 774                                           i18n.getString("wizard.fileNotFound.title"),
 775                                           JOptionPane.ERROR_MESSAGE);
 776         }
 777         catch (IOException e) {
 778             JOptionPane.showMessageDialog(main,
 779                                           i18n.getString("wizard.badFile.txt", e.getMessage()),
 780                                           i18n.getString("wizard.badFile.title"),
 781                                           JOptionPane.ERROR_MESSAGE);
 782         }
 783     }
 784 
 785     /**
 786      * Handle the "save" action
 787      */
 788     private void performSave() {
 789         questionPanel.saveCurrentResponse();
 790         // save with current file
 791         if (currFile == null)
 792             performSaveAs();
 793         else
 794             performSaveInternal(currFile);
 795     }
 796 
 797     /**
 798      * Handle the "save as" action
 799      */
 800     private void performSaveAs() {
 801         questionPanel.saveCurrentResponse();
 802         JFileChooser chooser = new JFileChooser();
 803         if (currFile != null) {
 804             // setCurrentDirectory required
 805             chooser.setCurrentDirectory(new File(currFile.getParent()));
 806             chooser.setSelectedFile(new File(currFile.getName()));
 807         }
 808         else {
 809             chooser.setCurrentDirectory(getUserDir());
 810         }
 811         chooser.setFileFilter(jtiFilter);
 812         int action = chooser.showSaveDialog(main);
 813         if (action != JFileChooser.APPROVE_OPTION)
 814             return;
 815         File f = ensureExtn(chooser.getSelectedFile(), ".jti");
 816         if (f.exists() && !okToOverwrite(f))
 817             return;
 818         performSaveInternal(f);
 819     }
 820 
 821     /**
 822      * Internal common routine for the save/saveAs actions
 823      */
 824     private void performSaveInternal(File f) {
 825         try {
 826             save(f);
 827             setFile(f);
 828         }
 829         catch (FileNotFoundException e) {
 830             JOptionPane.showMessageDialog(main,
 831                                           i18n.getString("wizard.fileNotFound.txt", e.getMessage()),
 832                                           i18n.getString("wizard.fileNotFound.title"),
 833                                           JOptionPane.ERROR_MESSAGE);
 834         }
 835         catch (IOException e) {
 836             JOptionPane.showMessageDialog(main,
 837                                           i18n.getString("wizard.badFile.txt", e.getMessage()),
 838                                           i18n.getString("wizard.badFile.title"),
 839                                           JOptionPane.ERROR_MESSAGE);
 840         }
 841     }
 842 
 843     /**
 844      * Get the user's current directory
 845      */
 846     private File getUserDir() {
 847         return new File(System.getProperty("user.dir"));
 848     }
 849 
 850     private JButton createButton(String uiKey, String actionCommand, ActionListener l) {
 851         JButton b = new JButton(createIcon(uiKey));
 852         b.setToolTipText(i18n.getString("wizard." + uiKey + ".tip"));
 853         b.setActionCommand(actionCommand);
 854         b.addActionListener(l);
 855         b.registerKeyboardAction(l, actionCommand, enterKey, JComponent.WHEN_FOCUSED);
 856         return b;
 857     }
 858 
 859     private Icon createIcon(String uiKey) {
 860         String iconResource = i18n.getString("wizard." + uiKey + ".icon");
 861         URL url = getClass().getResource(iconResource);
 862         return (url == null ? null : new ImageIcon(url));
 863     }
 864 
 865     /**
 866      * Create a menu according to an array of data
 867      * @title the title for the menu
 868      * @menuData the data for the menu; one element per menu item; an element can be
 869      * one of
 870      * <dl>
 871      * <dt> null
 872      *   <dd> a separator
 873      * <dt> an array of two strings
 874      *   <dd> a menu item, whose name is the first string, and whose action is the second
 875      * </dl>
 876      */
 877     private JMenu createMenu(String uiKey, String[][] menuData, ActionListener l) {
 878         JMenu m = new JMenu(i18n.getString("wizard." + uiKey + ".menu"));
 879         m.setName("wizard." + uiKey);
 880         m.setMnemonic(i18n.getString("wizard." + uiKey + ".mne").charAt(0));
 881         for (int i = 0; i < menuData.length; i++) {
 882             String[] data = menuData[i];
 883             if (data == null)
 884                 m.addSeparator();
 885             else {
 886                 JMenuItem mi = createMenuItem(uiKey, data[0], data[1], l);
 887                 if (data.length > 2) {
 888                     KeyStroke accel = KeyStroke.getKeyStroke(data[2]);
 889                     mi.setAccelerator(accel);
 890                 }
 891                 m.add(mi);
 892             }
 893         }
 894         return m;
 895     }
 896 
 897     private JMenuItem createMenuItem(String uiKey, String name, String actionCommand, ActionListener l) {
 898         JMenuItem item = new JMenuItem(i18n.getString("wizard." + uiKey + "." + name + ".mit"));
 899         item.setName(name);
 900         item.setMnemonic(i18n.getString("wizard." + uiKey + "." + name + ".mne").charAt(0));
 901         item.setActionCommand(actionCommand);
 902         item.addActionListener(l);
 903         return item;
 904     }
 905 
 906     private JToggleButton createToggle(String uiKey, String actionCommand, ActionListener l) {
 907         JToggleButton b = new JToggleButton(createIcon(uiKey)) {
 908             public Insets getInsets() {
 909                 return (nextBtn == null ? super.getInsets() : nextBtn.getInsets()); // !!
 910             }
 911         };
 912         b.setToolTipText(i18n.getString("wizard." + uiKey + ".tip"));
 913         b.setActionCommand(actionCommand);
 914         b.addActionListener(l);
 915         b.registerKeyboardAction(l, actionCommand, enterKey, JComponent.WHEN_FOCUSED);
 916         return b;
 917     }
 918 
 919     private File ensureExtn(File f, String extn) {
 920         if (f.getName().endsWith(extn))
 921             return f;
 922         else
 923             return new File(f.getPath() + extn);
 924     }
 925 
 926     private boolean okToContinue() {
 927         int response =
 928             JOptionPane.showConfirmDialog(main,
 929                                          i18n.getString("wizard.unsavedAnswers.txt"),
 930                                          i18n.getString("wizard.unsavedAnswers.title"),
 931                                          JOptionPane.YES_NO_OPTION);
 932         return (response == JOptionPane.YES_OPTION);
 933     }
 934 
 935     private boolean okToOverwrite(File f) {
 936         int response =
 937             JOptionPane.showConfirmDialog(main,
 938                                          i18n.getString("wizard.overwrite.txt", f),
 939                                          i18n.getString("wizard.overwrite.title"),
 940                                          JOptionPane.YES_NO_OPTION);
 941         return (response == JOptionPane.YES_OPTION);
 942     }
 943 
 944     private ActionListener performer = new ActionListener() {
 945         public void actionPerformed(ActionEvent e) {
 946             perform(e.getActionCommand());
 947         }
 948     };
 949 
 950     private Interview interview;
 951     private Exporter[] exporters;
 952     private String title;
 953     private JMenuBar menuBar;
 954     private JMenu fileMenu;
 955     //private JPanel main;
 956     private JComponent main;
 957     private JPanel body;
 958     private PathPanel pathPanel;
 959     private QuestionPanel questionPanel;
 960     private InfoPanel infoPanel;
 961     private JToolBar buttonPanel;
 962     private JButton cancelBtn;
 963     private JButton backBtn;
 964     private JButton nextBtn;
 965     private JButton okBtn;
 966     private JToggleButton infoBtn;
 967     private Window window;
 968     private ActionListener okListener;
 969     private SearchDialog searchDialog;
 970     private boolean initialInfoVisible = true;
 971     private Listener listener = new Listener();
 972 
 973     // help for Help menu and context sensitive help (F1)
 974     private HelpSet helpHelpSet;
 975     private HelpBroker helpHelpBroker;
 976     private String helpHelpPrefix;
 977     private JMenu helpMenu;
 978 
 979     private File currFile;
 980     private File defaultFile;
 981     private boolean exitOnClose;
 982 
 983     private final FileFilter jtiFilter = new ExtensionFileFilter(".jti");
 984     private final FileFilter htmlFilter =
 985         new ExtensionFileFilter(new String[] {".htm", ".html"});
 986 
 987     private static final KeyStroke enterKey = KeyStroke.getKeyStroke("ENTER");
 988 
 989     private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle();
 990 
 991     private static final String[][] fileMenuData = {
 992         {"new", "performNew"},
 993         {"open", "performOpen"},
 994         {"save", "performSave"},
 995         {"saveAs", "performSaveAs"},
 996         null,
 997         {"exportLog", "performExportLog"}
 998     };
 999 
1000     private static final String[][] helpMenuData = {
1001         {"help", "performHelp", "F1"}
1002     };
1003 
1004     private static final String[][] searchMenuData = {
1005         {"find", "performFind", "control F"},
1006         {"findNext", "performFindNext", "F3"},
1007     };
1008 
1009     private class ExtensionFileFilter extends FileFilter {
1010         ExtensionFileFilter(String extn) {
1011             this.extns = new String[] {extn};
1012         }
1013 
1014         ExtensionFileFilter(String[] extns) {
1015             this.extns = extns;
1016         }
1017 
1018 
1019         ExtensionFileFilter(String[] extns, String description) {
1020             this.extns = extns;
1021             this.description = description;
1022         }
1023 
1024         public boolean accept(File f) {
1025             if (f.isDirectory())
1026                 return true;
1027             for (int i = 0; i < extns.length; i++)
1028                 if (f.getName().endsWith(extns[i]))
1029                     return true;
1030             return false;
1031         }
1032 
1033         public String getDescription() {
1034             if (description == null) {
1035                 StringBuffer sb = new StringBuffer("wizard.extn");
1036                 if (extns.length == 0)
1037                     sb.append(".allFiles");
1038                 else {
1039                     for (int i = 0; i < extns.length; i++)
1040                         sb.append(extns[i]);
1041                 }
1042                 description = i18n.getString(sb.toString());
1043             }
1044             return description;
1045         }
1046 
1047         private String[] extns;
1048         private String description;
1049     }
1050 
1051     private class ExportMenu extends JMenu implements ActionListener, PopupMenuListener {
1052         ExportMenu(Exporter[] exporters) {
1053             super(i18n.getString("wizard.export.menu"));
1054             setName("export");
1055             setMnemonic(i18n.getString("wizard.export.mne").charAt(0));
1056             for (int i = 0; i < exporters.length; i++) {
1057                 JMenuItem mi = new JMenuItem(exporters[i].getName());
1058                 mi.putClientProperty("exporter", exporters[i]);
1059                 mi.setActionCommand("performGenericExport");
1060                 mi.addActionListener(this);
1061                 add(mi);
1062             }
1063             getPopupMenu().addPopupMenuListener(this);
1064         }
1065 
1066         public void actionPerformed(ActionEvent ev) {
1067             questionPanel.saveCurrentResponse();
1068             JMenuItem mi = (JMenuItem)(ev.getSource());
1069             Exporter e = (Exporter)(mi.getClientProperty("exporter"));
1070             export(e);
1071         }
1072 
1073         public void popupMenuCanceled(PopupMenuEvent e) {
1074         }
1075 
1076         public void popupMenuWillBecomeInvisible(PopupMenuEvent ev) {
1077         }
1078 
1079         public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
1080             JPopupMenu m = (JPopupMenu)(ev.getSource());
1081             for (int i = 0; i < m.getComponentCount(); i++) {
1082                 JMenuItem mi = (JMenuItem)(m.getComponent(i));
1083                 if (mi != null) {
1084                     Exporter e = (Exporter)(mi.getClientProperty("exporter"));
1085                     if (e != null)
1086                         mi.setEnabled(e.isExportable());
1087                 }
1088             }
1089         }
1090 
1091         private void export(Exporter e) {
1092             JFileChooser exportChooser = new JFileChooser();
1093             if (currFile != null) {
1094                 // setCurrentDirectory required
1095                 exportChooser.setCurrentDirectory(new File(currFile.getParent()));
1096                 String[] extns = e.getFileExtensions();
1097                 int dot = currFile.getName().lastIndexOf(".");
1098                 if (dot != -1 && extns != null && extns.length > 0) {
1099                     File f = new File(currFile.getName().substring(0, dot) + extns[0]);
1100                     exportChooser.setSelectedFile(f);
1101                 }
1102             }
1103             else {
1104                 exportChooser.setCurrentDirectory(getUserDir());
1105             }
1106             exportChooser.setApproveButtonText(i18n.getString("wizard.exportChooser.export"));
1107             String[] extns = e.getFileExtensions();
1108             String desc = e.getFileDescription();
1109             exportChooser.setFileFilter(new ExtensionFileFilter(extns, desc));
1110             int action = exportChooser.showSaveDialog(main);
1111             if (action != JFileChooser.APPROVE_OPTION)
1112                 return;
1113             try {
1114                 File f = ensureExtn(exportChooser.getSelectedFile(), extns[0]);
1115                 if (f.exists() && !okToOverwrite(f))
1116                     return;
1117                 e.export(f);
1118             }
1119             catch (IOException ex) {
1120                 JOptionPane.showMessageDialog(main,
1121                                               i18n.getString("wizard.exportError.txt", ex.getMessage()),
1122                                               i18n.getString("wizard.exportError.title"),
1123                                               JOptionPane.ERROR_MESSAGE);
1124             }
1125             catch (Interview.Fault ex) {
1126                 JOptionPane.showMessageDialog(main,
1127                                               i18n.getString("wizard.exportError.txt", ex.getMessage()),
1128                                               i18n.getString("wizard.exportError.title"),
1129                                               JOptionPane.ERROR_MESSAGE);
1130             }
1131         }
1132     }
1133 
1134     private class Listener implements AncestorListener, Interview.Observer
1135     {
1136         // ---------- from AncestorListener -----------
1137 
1138         public void ancestorAdded(AncestorEvent e) {
1139             interview.addObserver(this);
1140             pathUpdated();
1141             currentQuestionChanged(interview.getCurrentQuestion());
1142         }
1143 
1144         public void ancestorMoved(AncestorEvent e) { }
1145 
1146         public void ancestorRemoved(AncestorEvent e) {
1147             interview.removeObserver(this);
1148         }
1149 
1150         //----- from Interview.Observer -----------
1151 
1152         public void pathUpdated() {
1153             okBtn.setEnabled(interview.isFinishable());
1154         }
1155 
1156         public void currentQuestionChanged(Question q) {
1157             backBtn.setEnabled(!interview.isFirst(q));
1158             nextBtn.setEnabled(!interview.isLast(q));
1159         }
1160     }
1161 }