1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2011, 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.Color;
  31 import java.awt.Component;
  32 import java.awt.Dimension;
  33 import java.awt.Font;
  34 import java.awt.GridBagConstraints;
  35 import java.awt.GridBagLayout;
  36 import java.awt.HeadlessException;
  37 import java.awt.KeyboardFocusManager;
  38 import java.awt.Rectangle;
  39 import java.awt.Toolkit;
  40 import java.awt.event.ActionEvent;
  41 import java.awt.event.KeyEvent;
  42 import java.net.URL;
  43 import java.util.HashMap;
  44 import java.util.Map;
  45 import javax.accessibility.AccessibleContext;
  46 import javax.swing.AbstractAction;
  47 import javax.swing.Action;
  48 import javax.swing.ActionMap;
  49 import javax.swing.BorderFactory;
  50 import javax.swing.FocusManager;
  51 import javax.swing.Icon;
  52 import javax.swing.ImageIcon;
  53 import javax.swing.InputMap;
  54 import javax.swing.JComponent;
  55 import javax.swing.JLabel;
  56 import javax.swing.JOptionPane;
  57 import javax.swing.JPanel;
  58 import javax.swing.JTextArea;
  59 import javax.swing.JTextField;
  60 import javax.swing.JViewport;
  61 import javax.swing.KeyStroke;
  62 import javax.swing.Scrollable;
  63 import javax.swing.SwingConstants;
  64 import javax.swing.event.AncestorEvent;
  65 import javax.swing.event.AncestorListener;
  66 
  67 import com.sun.interview.ChoiceArrayQuestion;
  68 import com.sun.interview.ChoiceQuestion;
  69 import com.sun.interview.ErrorQuestion;
  70 import com.sun.interview.FileListQuestion;
  71 import com.sun.interview.FileQuestion;
  72 import com.sun.interview.FloatQuestion;
  73 import com.sun.interview.InetAddressQuestion;
  74 import com.sun.interview.IntQuestion;
  75 import com.sun.interview.Interview;
  76 import com.sun.interview.ListQuestion;
  77 import com.sun.interview.NullQuestion;
  78 import com.sun.interview.PropertiesQuestion;
  79 import com.sun.interview.Question;
  80 import com.sun.interview.StringQuestion;
  81 import com.sun.interview.StringListQuestion;
  82 import com.sun.interview.TreeQuestion;
  83 import com.sun.interview.YesNoQuestion;
  84 import java.util.EventListener;
  85 import javax.swing.JEditorPane;
  86 import javax.swing.JScrollPane;
  87 
  88 /**
  89  * A panel which implements a {@link com.sun.interview.wizard.Wizard wizard}
  90  * that asks a series of {@link com.sun.interview.Question questions}
  91  * embodied in an {@link Interview interview}.
  92  */
  93 class QuestionPanel extends JPanel
  94     implements Scrollable
  95 {
  96 
  97     /**
  98      * Create a panel in which to display the questions of the interview.
  99      * The interview to be run.
 100      */
 101     QuestionPanel(Interview i) {
 102         interview = i;
 103 
 104         initRenderers();
 105         initGUI();
 106 
 107         addAncestorListener(listener);
 108 
 109     }
 110 
 111 
 112     // ---------- Component stuff ---------------------------------------
 113 
 114     public Dimension getPreferredSize() {
 115         Dimension d = super.getPreferredSize();
 116         d.height = Math.max(d.height, PREFERRED_HEIGHT * DOTS_PER_INCH);
 117         d.width = Math.max(d.width, PREFERRED_WIDTH * DOTS_PER_INCH);
 118         return d;
 119     }
 120 
 121     // ---------- Scrollable stuff ---------------------------------------
 122 
 123     public Dimension getPreferredScrollableViewportSize() {
 124         Dimension maxD = new Dimension(PREFERRED_WIDTH * DOTS_PER_INCH, PREFERRED_HEIGHT * DOTS_PER_INCH);
 125         Dimension ps = getPreferredSize();
 126         ps.width = Math.min(ps.width, maxD.width);
 127         ps.height = Math.min(ps.height, maxD.height);
 128         return ps;
 129     }
 130 
 131     public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
 132         switch(orientation) {
 133         case SwingConstants.VERTICAL:
 134             return visibleRect.height / 10;
 135         case SwingConstants.HORIZONTAL:
 136             return visibleRect.width / 10;
 137         default:
 138             throw new IllegalArgumentException("Invalid orientation: " + orientation);
 139         }
 140     }
 141 
 142     public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
 143         switch(orientation) {
 144         case SwingConstants.VERTICAL:
 145             return visibleRect.height;
 146         case SwingConstants.HORIZONTAL:
 147             return visibleRect.width;
 148         default:
 149             throw new IllegalArgumentException("Invalid orientation: " + orientation);
 150         }
 151     }
 152 
 153     public boolean getScrollableTracksViewportHeight() {
 154         // We can't use getPreferred size here, because of situation, when
 155         // getPreferredSize() gives default values. In this case, and if horizontal scroll exists,
 156         // viewport height becomes less than getPreferredSize().height, but may be enough for components
 157         // without any scrolling
 158         if(getParent() instanceof JViewport)
 159                 return getParent().getHeight() > getPreferredSize().height;
 160         else
 161             return false;
 162     }
 163 
 164     public final boolean getScrollableTracksViewportWidth() {
 165 
 166 //        if (currentRenderer instanceof SizeSensitiveQuestionRenderer) {
 167 //            Dimension d;
 168 //            int visWidth = ((JViewport)getParent()).getExtentSize().width;
 169 //            int requiredWidth = valuePanel.getPreferredSize().width + TEXT_AREA_INSETS_LEFT_RIGHT*2;
 170 //            if(requiredWidth < visWidth) {
 171 ////                d = this.getPreferredSize();
 172 ////                this.setPreferredSize(new Dimension(visWidth,
 173 ////                        super.getPreferredSize().height));
 174 ////                d = this.getPreferredSize();
 175 //                setHorScrollStatus(false);
 176 //                return true;
 177 //            }
 178 //            else {
 179 ////                d = this.getPreferredSize();
 180 ////                this.setPreferredSize(new Dimension(requiredWidth,
 181 ////                        super.getPreferredSize().height));
 182 ////                d = this.getPreferredSize();
 183 //                setHorScrollStatus(true);
 184 //                return false;
 185 //            }
 186 ////            setHorScrollStatus(true);
 187 ////            return false;
 188 //        }
 189 //        else {
 190 ////            this.setPreferredSize(this.getSize());
 191 //            setHorScrollStatus(false);
 192             return true;
 193 //        }
 194     }
 195 
 196     // ---------- end of Scrollable stuff -----------------------------------
 197 
 198     void setNextAction(Action nextAction) {
 199         this.nextAction = nextAction;
 200     }
 201 
 202     void saveCurrentResponse() /*throws Interview.Fault*/ {
 203         if (valueSaver != null)
 204             valueSaver.run();
 205     }
 206 
 207     boolean isTagVisible() {
 208         return propsPanel.isVisible();
 209     }
 210 
 211     void setTagVisible(boolean v) {
 212         propsPanel.setVisible(v);
 213     }
 214 
 215     private void initRenderers() {
 216         renderers = new HashMap<>();
 217         renderers.put(ChoiceQuestion.class, new ChoiceQuestionRenderer());
 218         renderers.put(ChoiceArrayQuestion.class, new ChoiceArrayQuestionRenderer());
 219         renderers.put(FileQuestion.class, new FileQuestionRenderer());
 220         renderers.put(FileListQuestion.class, new FileListQuestionRenderer());
 221         renderers.put(FloatQuestion.class, new FloatQuestionRenderer());
 222         renderers.put(IntQuestion.class, new IntQuestionRenderer());
 223         renderers.put(InetAddressQuestion.class, new InetAddressQuestionRenderer());
 224         renderers.put(ListQuestion.class, new ListQuestionRenderer());
 225         renderers.put(NullQuestion.class, new NullQuestionRenderer());
 226         renderers.put(PropertiesQuestion.class, new PropertiesQuestionRenderer());
 227         renderers.put(StringQuestion.class, new StringQuestionRenderer());
 228         renderers.put(StringListQuestion.class, new StringListQuestionRenderer());
 229         renderers.put(TreeQuestion.class, new TreeQuestionRenderer());
 230         renderers.put(YesNoQuestion.class, new YesNoQuestionRenderer());
 231         setCustomRenderers(new HashMap<Class<? extends Question>, QuestionRenderer>());
 232     }
 233 
 234     /**
 235      * Create the basic GUI infrastructure. The content of the
 236      * various areas are filled in from the questions that are
 237      * asked.
 238      */
 239     private void initGUI() {
 240         /*
 241                 +---------------+-------------------------------+
 242                 |               |       title                   |
 243                 |               +-------------------------------+
 244                 |               |       text area               |
 245                 |   graphic     |                               |
 246                 |               +-------------------------------+
 247                 |               |                               |
 248                 |               |       value                   |
 249                 |               |                               |
 250                 |               +-------------------------------+
 251                 |               |       msg                     |
 252                 |               +-------------------------------+
 253                 |               |       tag info (optional)     |
 254                 +---------------+-------------------------------+
 255         */
 256         setInfo(this, "qu", false);
 257         setLayout(new GridBagLayout());
 258         GridBagConstraints c = new GridBagConstraints();
 259 
 260         graphicLabel = new JLabel();
 261         setInfo(graphicLabel, "qu.icon", false);
 262         graphicLabel.setFocusable(false);
 263         if (interview != null) {
 264             URL u = interview.getDefaultImage();
 265             if (u != null)
 266                 graphicLabel.setIcon(new ImageIcon(u));
 267         }
 268         c.anchor = GridBagConstraints.CENTER;
 269         c.gridheight = GridBagConstraints.REMAINDER;
 270         add(graphicLabel, c);
 271 
 272         titleField = new JTextField();
 273         setInfo(titleField, "qu.title", true);
 274         titleField.setEditable(false);
 275         titleField.setBackground(new Color(102, 102, 153));//titleField.setBackground(MetalLookAndFeel.getPrimaryControlDarkShadow());
 276         titleField.setForeground(Color.WHITE);//titleField.setForeground(MetalLookAndFeel.getWindowBackground());
 277         Font f = titleField.getFont();//Font f = MetalLookAndFeel.getSystemTextFont();
 278         titleField.setFont(f.deriveFont(f.getSize() * 1.5f));
 279         c.fill = GridBagConstraints.HORIZONTAL;
 280         c.gridwidth = GridBagConstraints.REMAINDER;
 281         c.gridheight = 1;
 282         c.weightx = 1;
 283         add(titleField, c);
 284 
 285         textArea = new JTextArea(3, 30);
 286         setInfo(textArea, "qu.text", true);
 287         textArea.setEditable(false);
 288         textArea.setLineWrap(true);
 289         textArea.setOpaque(false);
 290         textArea.setBackground(new Color(255, 255, 255, 0));
 291         textArea.setBorder(null);
 292         textArea.setWrapStyleWord(true);
 293         // override JTextArea focus traversal keys, resetting them to
 294         // the Component default (i.e. the same as for the parent.)
 295         textArea.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null);
 296         textArea.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null);
 297         // set enter to be same as Next
 298         {
 299             InputMap im = textArea.getInputMap();
 300             im.put(enterKey, "next");
 301             ActionMap am = textArea.getActionMap();
 302             am.put("next", valueAction);
 303         }
 304         c.insets.top = TEXT_AREA_INSETS_TOP;
 305         c.insets.left = c.insets.right = TEXT_AREA_INSETS_LEFT_RIGHT;
 306         c.insets.bottom = TEXT_AREA_INSETS_BOTTOM;
 307         c.fill = GridBagConstraints.BOTH;
 308         add(textArea, c);
 309 
 310 
 311         valuePanel = new JPanel(new BorderLayout());
 312         setInfo(valuePanel, "qu.vp", false);
 313         valuePanel.setOpaque(true);
 314         // set default action for enter to be same as Next
 315         {
 316             InputMap im = valuePanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 317             im.put(enterKey, "next");
 318             ActionMap am = valuePanel.getActionMap();
 319             am.put("next", valueAction);
 320         }
 321 
 322         c.insets.top = VALUE_PANEL_INSETS_TOP;
 323         c.weighty = 1;
 324         c.fill = GridBagConstraints.BOTH;
 325         c.insets.bottom = VALUE_PANEL_INSETS_BOTTOM;
 326         add(valuePanel, c);
 327 
 328         c.fill = GridBagConstraints.BOTH;
 329         valueMessageField = new JTextField();
 330         setInfo(valueMessageField, "qu.vmsg", false);
 331         valueMessageField.setEditable(false);
 332         valueMessageField.setOpaque(false);
 333         valueMessageField.setFont(valueMessageField.getFont().deriveFont(Font.BOLD));
 334         valueMessageField.setBorder(null);
 335         c.insets.top = VALUE_MESSAGE_FIELD_INSETS_TOP;
 336         c.insets.bottom = VALUE_MESSAGE_FIELD_INSETS_BOTTOM;
 337         c.weighty = 0;
 338         add(valueMessageField, c);
 339 
 340         propsPanel = new JPanel(new BorderLayout());
 341         propsPanel.setBorder(BorderFactory.createEtchedBorder());
 342                 // replace by titled border "properties" if we get more than one...
 343         propsPanel.setName("qu.props.pnl");
 344         propsPanel.setFocusable(false);
 345         JLabel tagLabel = new JLabel(i18n.getString("qu.tag.lbl"));
 346         setInfo(tagLabel, "qu.tag.lbl", true);
 347         tagLabel.setDisplayedMnemonic(i18n.getString("qu.tag.mne").charAt(0));
 348         tagLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 7));
 349         propsPanel.add(tagLabel, BorderLayout.WEST);
 350         tagField = new JTextField();
 351         tagField.setName("qu.tag.fld");
 352         tagField.setEditable(false);
 353         tagField.setBorder(null);
 354         tagLabel.setLabelFor(tagField);
 355         propsPanel.add(tagField, BorderLayout.CENTER);
 356         propsPanel.setVisible(false);
 357         c.insets.top = PROPS_PANEL_INSETS_TOP;
 358         c.insets.bottom = PROPS_PANEL_INSETS_BOTTOM;
 359         add(propsPanel, c);
 360 
 361         ActionMap actionMap = getActionMap();
 362 
 363         actionMap.put("hideProps", new AbstractAction() {
 364                 public void actionPerformed(ActionEvent e) {
 365                     propsPanel.setVisible(false);
 366                 }
 367             });
 368 
 369         actionMap.put("showProps", new AbstractAction() {
 370                 public void actionPerformed(ActionEvent e) {
 371                     propsPanel.setVisible(true);
 372                 }
 373             });
 374 
 375         actionMap.put("toggleProps", new AbstractAction() {
 376                 public void actionPerformed(ActionEvent e) {
 377                     propsPanel.setVisible(!propsPanel.isVisible());
 378                 }
 379             });
 380 
 381         InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 382         inputMap.put(KeyStroke.getKeyStroke("ctrl T"), "toggleProps");
 383 
 384     }
 385 
 386     private void setInfo(JComponent jc, String uiKey, boolean addToolTip) {
 387         jc.setName(uiKey);
 388         AccessibleContext ac = jc.getAccessibleContext();
 389         ac.setAccessibleName(i18n.getString(uiKey + ".name"));
 390         if (addToolTip) {
 391             String tip = i18n.getString(uiKey + ".tip");
 392             jc.setToolTipText(tip);
 393             ac.setAccessibleDescription(tip);
 394         }
 395         else
 396             ac.setAccessibleDescription(i18n.getString(uiKey + ".desc"));
 397     }
 398 
 399     /**
 400      * Show a question. The appropriate showXXX method
 401      * is called, depending on the question, and then a response
 402      * is awaited.
 403      */
 404     public void showQuestion(Question q) {
 405         //System.err.println("QP.showQuestion " + q.getTag() + " " + q);
 406         if (q instanceof ErrorQuestion) {
 407             showErrorQuestion((ErrorQuestion) q);
 408             try {
 409                 // no current response to save :-)
 410                 interview.prev();
 411             }
 412             catch (Interview.Fault ignore) {
 413             }
 414             return;
 415         }
 416 
 417         URL u = q.getImage();
 418         final Icon icon = (u == null ? null : new ImageIcon(u));
 419 
 420         if (icon != null)
 421             graphicLabel.setIcon(icon);
 422 
 423         titleField.setText(q.getSummary());
 424         textArea.setText(q.getText());
 425         tagField.setText(q.getTag());
 426 
 427         boolean focus = anyChildHasFocus(valuePanel);
 428         valuePanel.removeAll();
 429 
 430         QuestionRenderer r = getRenderer(q);
 431 
 432         //------------------------------------
 433 
 434         if (r == null) {
 435             //System.err.println("no renderer for " + q.getTag() + " [" + q.getClass().getName() + "]");
 436             valueSaver = null;
 437         }
 438         else {
 439             JComponent rc = r.getQuestionRendererComponent(q, valueAction);
 440             if (rc == null) {
 441                 valueSaver = null;
 442                 if (focus) {
 443                     // no response area, so put focus back on question text
 444                     textArea.requestFocus();
 445                 }
 446             }
 447             else {
 448                 if (rc.getName() == null)
 449                     rc.setName(r.getClass().getName());
 450 
 451                 valueSaver = (Runnable) (rc.getClientProperty(QuestionRenderer.VALUE_SAVER));
 452                 //System.err.println("QP.showQuestion valueSaver=" + valueSaver);
 453                 valuePanel.add(rc);
 454                 if (focus) {
 455                     //System.err.println("QP.showQuestion: setFocus");
 456                     FocusManager fm = FocusManager.getCurrentManager();
 457                     //fm.focusNextComponent(valuePanel);
 458                     fm.focusNextComponent(textArea);
 459                 }
 460             }
 461         }
 462 
 463         if (q.isValueAlwaysValid())
 464             valueMessageField.setVisible(false);
 465         else {
 466             showValueMessage(null);
 467             valueMessageField.setVisible(true);
 468         }
 469 
 470         // relayout the GUI
 471         revalidate();
 472         repaint();
 473 
 474         currentRenderer = r;
 475         currentQuestion = q;
 476     }
 477 
 478     private void showErrorQuestion(ErrorQuestion q) throws HeadlessException {
 479 
 480         JEditorPane ePane = new JEditorPane(q.getTextMimeType(), q.getText());
 481         ePane.setEditable(false);
 482 
 483         final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
 484         final Dimension minS = new Dimension(350, 100);
 485         final Dimension maxS = new Dimension(Math.min(2*(screen.width/3), 800), Math.min(2*(screen.height/3), 600));
 486         Dimension p =  ePane.getPreferredSize();
 487 
 488         p.setSize(Math.max(p.width, minS.width), Math.max(p.height, minS.height));
 489         p.setSize(Math.min(p.width, maxS.width), Math.min(p.height, maxS.height));
 490         ePane.setPreferredSize(p);
 491 
 492         ePane.setCaretPosition(0);
 493         JScrollPane sPane = new JScrollPane(ePane);
 494         sPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
 495         sPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
 496 
 497         JOptionPane.showMessageDialog(this, sPane, q.getSummary(), JOptionPane.ERROR_MESSAGE);
 498 
 499     }
 500 
 501     /**
 502      * This method invokes when config editor is going to be closed.
 503      * This made to allow components handle feature closing
 504      */
 505     public void prepareClosing() {
 506         if(currentRenderer instanceof PropertiesQuestionRenderer) {
 507             AncestorEvent e = new AncestorEvent(this, AncestorEvent.ANCESTOR_REMOVED,
 508                                     valuePanel, valuePanel.getParent());
 509             Component[] childs = valuePanel.getComponents();
 510             for(int i = 0; i < childs.length; i++) {
 511                 EventListener[] l = childs[i].getListeners(AncestorListener.class);
 512                 for(int j = 0; j < l.length; j++) {
 513                     if(l[i] instanceof AncestorListener) {
 514                         ((AncestorListener)l[i]).ancestorRemoved(e);
 515                     }
 516                 }
 517             }
 518         }
 519     }
 520 
 521     public void showValueInvalidMessage() {
 522         String msg = (currentRenderer == null
 523                       ? null
 524                       : currentRenderer.getInvalidValueMessage(currentQuestion));
 525         showValueMessage((msg == null ? INVALID_VALUE : msg), INVALID_VALUE_COLOR);
 526     }
 527 
 528     private void showValueMessage(String msg) {
 529         showValueMessage(msg, Color.BLACK);//showValueMessage(msg, MetalLookAndFeel.getBlack());
 530     }
 531 
 532     private void showValueMessage(String msg, Color c) {
 533         if (msg == null || msg.length() == 0) {
 534             valueMessageField.setText("");
 535             valueMessageField.setEnabled(false);
 536         }
 537         else {
 538             valueMessageField.setForeground(c);
 539             valueMessageField.setText(msg);
 540             valueMessageField.setEnabled(true);
 541         }
 542     }
 543 
 544     private QuestionRenderer getRenderer(Question q) {
 545         QuestionRenderer result = null;
 546         if (customRenderers != null) {
 547             result = getRenderer(q, customRenderers);
 548         }
 549         if (result == null) {
 550             result = getRenderer(q, renderers);
 551         }
 552         return result;
 553     }
 554 
 555     private QuestionRenderer getRenderer(Question q, Map<Class<? extends Question>, QuestionRenderer> rendMap) {
 556         for (Class<?> c = q.getClass(); c != null; c = c.getSuperclass()) {
 557             QuestionRenderer r = rendMap.get(c);
 558             if (r != null)
 559                 return r;
 560         }
 561         return null;
 562     }
 563 
 564 
 565     private boolean anyChildHasFocus(JPanel p) {
 566         if (p.hasFocus())
 567             return true;
 568 
 569         for (int i = 0; i < p.getComponentCount(); i++) {
 570             Component c = (p.getComponent(i));
 571             if ((c instanceof JComponent && c.hasFocus())
 572                 || (c instanceof JPanel && anyChildHasFocus((JPanel)c)))
 573                 return true;
 574         }
 575         return false;
 576     }
 577 
 578     private Interview interview;
 579     private Question currentQuestion;
 580     private QuestionRenderer currentRenderer;
 581     private JLabel graphicLabel;
 582     private JTextField titleField;
 583     private JTextArea textArea;
 584     private JPanel valuePanel;
 585     private Runnable valueSaver;
 586     private JTextField valueMessageField;
 587     private JPanel propsPanel;
 588     private JTextField tagField;
 589     private Map<Class<? extends Question>, QuestionRenderer> renderers;
 590     private Map<Class<? extends Question>, QuestionRenderer> customRenderers;
 591     private Listener listener = new Listener();
 592 
 593     private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle();
 594     private static String INVALID_VALUE = i18n.getString("qu.invalidValue.txt");
 595     private static Color INVALID_VALUE_COLOR = i18n.getErrorColor();
 596 
 597     private KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
 598     private Action valueAction = new AbstractAction() {
 599             public void actionPerformed(ActionEvent e) {
 600                 String cmd = e.getActionCommand();
 601                 if (cmd.equals(QuestionRenderer.EDITED))
 602                     showValueMessage(null);
 603                 else {
 604                     if (nextAction != null) {
 605                         nextAction.actionPerformed(e);
 606                     }
 607                     else {
 608                         // default old behavior
 609                         try {
 610                             saveCurrentResponse();
 611                             interview.next();
 612                         }
 613                         catch (Interview.Fault ex) {
 614                             // exception normally means no more questions,
 615                             // which should only be  because the value of the current
 616                             // question is invalid
 617                             // e.printStackTrace();
 618                             // QuestionPanel.this.getToolkit().beep();
 619                             showValueInvalidMessage();
 620                         }
 621                     }
 622                 }
 623             }
 624 
 625     };
 626 
 627     private Action nextAction; // optionally settable
 628 
 629     private static final int PREFERRED_HEIGHT = 3; // inches
 630     private static final int PREFERRED_WIDTH = 4; // inches
 631     private static final int DOTS_PER_INCH = Toolkit.getDefaultToolkit().getScreenResolution();
 632     private static final int TEXT_AREA_INSETS_TOP = 20;
 633 
 634     private static final int TEXT_AREA_INSETS_LEFT_RIGHT = 10;
 635     private static final int TEXT_AREA_INSETS_BOTTOM = 10;
 636 
 637     private static final int VALUE_PANEL_INSETS_TOP = 0;
 638     private static final int VALUE_PANEL_INSETS_BOTTOM = 10;
 639 
 640     private static final int VALUE_MESSAGE_FIELD_INSETS_TOP = 0;
 641     private static final int VALUE_MESSAGE_FIELD_INSETS_BOTTOM = 0;
 642 
 643     private static final int PROPS_PANEL_INSETS_TOP = 0;
 644     private static final int PROPS_PANEL_INSETS_BOTTOM = 10;
 645 
 646     public void setCustomRenderers(Map<Class<? extends Question>, QuestionRenderer> customRenderers) {
 647         this.customRenderers = customRenderers;
 648     }
 649 
 650 
 651     private class Listener
 652         implements AncestorListener, Interview.Observer
 653     {
 654 
 655         // ---------- AncestorListener
 656 
 657         public void ancestorAdded(AncestorEvent e) {
 658             interview.addObserver(this);
 659             showQuestion(interview.getCurrentQuestion());
 660         }
 661 
 662         public void ancestorMoved(AncestorEvent e) { }
 663 
 664         public void ancestorRemoved(AncestorEvent e) {
 665             interview.removeObserver(this);
 666         }
 667 
 668         // ---------- Interview.Observer ----------
 669 
 670         public void pathUpdated() {
 671             // if path is updated as a result of refresh, clear the error message
 672             showValueMessage(null);
 673         }
 674 
 675         public void currentQuestionChanged(Question q) {
 676             showQuestion(q);
 677         }
 678 
 679         public void finished() { }
 680 
 681     }
 682 
 683 }