1 /*
   2  * CDDL HEADER START
   3  *
   4  * The contents of this file are subject to the terms of the
   5  * Common Development and Distribution License, Version 1.0 only
   6  * (the "License").  You may not use this file except in compliance
   7  * with the License.
   8  *
   9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
  10  * or http://www.opensolaris.org/os/licensing.
  11  * See the License for the specific language governing permissions
  12  * and limitations under the License.
  13  *
  14  * When distributing Covered Code, include this CDDL HEADER in each
  15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
  16  * If applicable, add the following below this CDDL HEADER, with the
  17  * fields enclosed by brackets "[]" replaced with your own identifying
  18  * information: Portions Copyright [yyyy] [name of copyright owner]
  19  *
  20  * CDDL HEADER END
  21  */
  22 /*
  23  * ident        "%Z%%M% %I%     %E% SMI"
  24  *
  25  * Copyright 1998-2002 by Sun Microsystems, Inc.  All rights reserved.
  26  * Use is subject to license terms.
  27  */
  28 package com.sun.dhcpmgr.ui;
  29 
  30 import javax.swing.*;
  31 import javax.swing.border.*;
  32 
  33 import java.awt.event.*;
  34 import java.awt.*;
  35 import java.util.*;
  36 import java.text.NumberFormat;
  37 
  38 // Interface for notification of button events
  39 interface WizButtonListener {
  40     public void buttonPressed(int buttonId);
  41     public static final int BACK = 0;
  42     public static final int FORWARD = 1;
  43     public static final int CANCEL = 2;
  44     public static final int HELP = 3;
  45     public static final int FINISH = 4;
  46 }
  47 
  48 /**
  49  * This is a widget for presenting a multi-step task, such as an installation
  50  * sequence or other task with many questions or multiple possible paths.  This
  51  * component displays as a dialog, with a contents outline on the left and a
  52  * panel for display of each step on the right, with a set of control buttons
  53  * at the bottom.  Typical usage is to subclass this class and implement a set
  54  * of WizardStep classes which provide the steps to be executed.
  55  *
  56  * @see WizardStep
  57  */
  58 public class Wizard extends JDialog {
  59 
  60     // Panel to manage display and processing of buttons at base of window
  61     class WizButtonPanel extends JPanel {
  62     
  63         // Adaptor class to catch all button presses and pass along to listeners
  64         class ButtonAdaptor implements ActionListener {
  65             public void actionPerformed(ActionEvent e) {
  66                 int buttonId = -1;
  67                 Object source = e.getSource();
  68                 if (source == backButton || source == backButton2) {
  69                     buttonId = WizButtonListener.BACK;
  70                 } else if (source == forwardButton) {
  71                     buttonId = WizButtonListener.FORWARD;
  72                 } else if (source == cancelButton || source == cancelButton2) {
  73                     buttonId = WizButtonListener.CANCEL;
  74                 } else if (source == helpButton || source == helpButton2) {
  75                     buttonId = WizButtonListener.HELP;
  76                 } else if (source == finishButton) {
  77                     buttonId = WizButtonListener.FINISH;
  78                 }
  79                 Enumeration en = listeners.elements();
  80                 while (en.hasMoreElements()) {
  81                     WizButtonListener l = (WizButtonListener)en.nextElement();
  82                     l.buttonPressed(buttonId);
  83                 }
  84             }
  85         }
  86         
  87         PreviousButton backButton, backButton2;
  88         NextButton forwardButton;
  89         JButton cancelButton, helpButton, finishButton;
  90         JButton cancelButton2, helpButton2;
  91         Vector listeners;
  92         ButtonAdaptor adaptor;
  93         JPanel innerPanel;
  94         
  95         public WizButtonPanel() {           
  96             super();
  97 
  98             setBorder(new EtchedBorder());
  99             // Right justify the buttons
 100             setLayout(new FlowLayout(FlowLayout.RIGHT));
 101             innerPanel = new JPanel(new CardLayout());
 102             
 103             // Create event handler
 104             adaptor = new ButtonAdaptor();
 105             listeners = new Vector();
 106 
 107             /*
 108              * Construct back buttons; need 2 for the separate cards used
 109              * in the button panel
 110              */
 111             backButton = new PreviousButton();
 112             backButton2 = new PreviousButton();
 113             backButton.addActionListener(adaptor);
 114             backButton2.addActionListener(adaptor);
 115             backButton.setAlignmentY(Component.CENTER_ALIGNMENT);
 116             backButton2.setAlignmentY(Component.CENTER_ALIGNMENT);
 117 
 118             // Construct forward button
 119             forwardButton = new NextButton();
 120             forwardButton.addActionListener(adaptor);
 121             forwardButton.setAlignmentY(Component.CENTER_ALIGNMENT);
 122            
 123             Mnemonic mnFinish = 
 124                 new Mnemonic(ResourceStrings.getString("finish_button"));
 125             finishButton = new JButton(mnFinish.getString());
 126             finishButton.setToolTipText(mnFinish.getString());
 127             finishButton.setMnemonic(mnFinish.getMnemonic());
 128             finishButton.addActionListener(adaptor);
 129             finishButton.setAlignmentY(Component.CENTER_ALIGNMENT);
 130         
 131             Mnemonic mnCancel = 
 132                 new Mnemonic(ResourceStrings.getString("cancel_button"));     
 133             cancelButton = new JButton(mnCancel.getString());    
 134             cancelButton.setToolTipText(mnCancel.getString());   
 135             cancelButton.setMnemonic(mnCancel.getMnemonic()); 
 136 
 137             cancelButton.addActionListener(adaptor);
 138             cancelButton.setAlignmentY(Component.CENTER_ALIGNMENT);
 139 
 140             cancelButton2 = new JButton(mnCancel.getString());
 141             cancelButton2.setToolTipText(mnCancel.getString());   
 142             cancelButton2.setMnemonic(mnCancel.getMnemonic()); 
 143 
 144             cancelButton2.addActionListener(adaptor);
 145             cancelButton2.setAlignmentY(Component.CENTER_ALIGNMENT);
 146 
 147             Mnemonic mnHelp = 
 148                 new Mnemonic(ResourceStrings.getString("help_button"));
 149             helpButton = new JButton(mnHelp.getString());
 150             helpButton.setToolTipText(mnHelp.getString());
 151             helpButton.setMnemonic(mnHelp.getMnemonic());
 152  
 153             helpButton.addActionListener(adaptor);
 154             helpButton.setAlignmentY(Component.CENTER_ALIGNMENT);
 155 
 156             helpButton2 = new JButton(mnHelp.getString());
 157             helpButton2.setToolTipText(mnHelp.getString());
 158             helpButton2.setMnemonic(mnHelp.getMnemonic());
 159 
 160             helpButton2.addActionListener(adaptor);
 161             helpButton2.setAlignmentY(Component.CENTER_ALIGNMENT);
 162             
 163             /*
 164              * Now create cards; we created two copies of buttons that
 165              * needed to be on both cards
 166              */
 167             Box box = Box.createHorizontalBox();
 168             box.add(Box.createHorizontalGlue());
 169             box.add(backButton);
 170             box.add(Box.createHorizontalStrut(5));
 171             box.add(forwardButton);
 172             box.add(Box.createHorizontalStrut(5));
 173             box.add(cancelButton);
 174             box.add(Box.createHorizontalStrut(5));
 175             box.add(helpButton);
 176             innerPanel.add(box, "normal");
 177             
 178             // Finish panel replaces the forward button with the finish button
 179             box = Box.createHorizontalBox();
 180             box.add(Box.createHorizontalGlue());
 181             box.add(backButton2);
 182             box.add(Box.createHorizontalStrut(5));
 183             box.add(finishButton);
 184             box.add(Box.createHorizontalStrut(5));
 185             box.add(cancelButton2);
 186             box.add(Box.createHorizontalStrut(5));
 187             box.add(helpButton2);
 188             innerPanel.add(box, "finish");
 189 
 190             add(innerPanel);
 191         }
 192         
 193         // Show the first step
 194         public void showFirst() {
 195             backButton.setEnabled(false); // Can't go backwards
 196             setForwardEnabled(false);
 197             getRootPane().setDefaultButton(forwardButton);
 198             forwardButton.requestFocus(true);
 199             ((CardLayout)innerPanel.getLayout()).show(innerPanel, "normal");
 200         }
 201         
 202         // Show the last step
 203         public void showLast() {
 204             backButton.setEnabled(true);
 205             setFinishEnabled(false);
 206             getRootPane().setDefaultButton(finishButton);
 207             finishButton.requestFocus(true);
 208             ((CardLayout)innerPanel.getLayout()).show(innerPanel, "finish");
 209         }
 210         
 211         // Show any other step
 212         public void showMiddle() {
 213             backButton.setEnabled(true);
 214             setForwardEnabled(false);
 215             getRootPane().setDefaultButton(forwardButton);
 216             cancelButton.requestFocus(true);
 217             ((CardLayout)innerPanel.getLayout()).show(innerPanel, "normal");
 218         }
 219         
 220         // Allow steps to control when user may advance to next step
 221         public void setForwardEnabled(boolean state) {
 222             forwardButton.setEnabled(state);
 223         }
 224         
 225         // Allow the final step to control when a user may complete the task
 226         public void setFinishEnabled(boolean state) {
 227             finishButton.setEnabled(state);
 228         }
 229         
 230         public void addWizButtonListener(WizButtonListener l) {
 231             listeners.addElement(l);
 232         }
 233         
 234         public void removeWizButtonListener(WizButtonListener l) {
 235             listeners.removeElement(l);
 236         }
 237     }
 238     
 239     /*
 240      * Panel to display the list of steps; we use a very custom JList
 241      * for this as it does reasonable rendering with minimal effort on our part.
 242      */
 243     class WizContentsPanel extends JPanel {
 244         
 245         // Data model for holding the description text displayed
 246         class ContentsModel extends AbstractListModel {
 247             Vector data;
 248             
 249             public ContentsModel() {
 250                 data = new Vector();
 251             }
 252             
 253             public Object getElementAt(int index) {
 254                 return data.elementAt(index);
 255             }
 256             
 257             public int getSize() {
 258                 return data.size();
 259             }
 260             
 261             public void addItem(String description) {
 262                 data.addElement(description);
 263                 fireIntervalAdded(this, data.size()-1, data.size()-1);
 264             }
 265         }
 266         
 267         // Class to render the cells in the list
 268         class ContentsRenderer implements ListCellRenderer {
 269             NumberFormat nf;
 270             
 271             public ContentsRenderer() {
 272                 nf = NumberFormat.getInstance();
 273             }
 274             
 275             public Component getListCellRendererComponent(JList list,
 276                     Object value, int index, boolean isSelected,
 277                     boolean cellHasFocus) {
 278 
 279                 // Format label properly for i18n
 280                 JTextArea text = new JTextArea((String)value, 2, 15);
 281                 text.setWrapStyleWord(true);
 282                 text.setLineWrap(true);
 283                 text.setOpaque(false);
 284                 text.setAlignmentY(Component.TOP_ALIGNMENT);
 285                 JLabel l = new JLabel(nf.format(index + 1));
 286                 l.setForeground(Color.black);
 287                 l.setAlignmentY(Component.TOP_ALIGNMENT);
 288                 
 289                 JPanel stepBox = new JPanel();
 290                 stepBox.setLayout(new BoxLayout(stepBox, BoxLayout.X_AXIS));
 291                 stepBox.add(l);
 292                 stepBox.add(Box.createHorizontalStrut(5));
 293                 stepBox.add(text);
 294                 stepBox.setBackground(list.getSelectionBackground());
 295                 // Selected component is opaque, others transparent
 296                 stepBox.setOpaque(isSelected);
 297                 return stepBox;
 298             }
 299         }
 300         
 301         /*
 302          * This class is defined strictly so that we can prevent
 303          * focus from ever reaching the steps list as it is a display-only
 304          * use of JList, not intended for any sort of input by the user.
 305          */
 306         class MyList extends JList {
 307             public MyList(ListModel m) {
 308                 super(m);
 309                 // Don't allow this list to be focused
 310                 setFocusable(false);
 311             }
 312             // Ignore mouse clicks so highlighted step can't be changed
 313             protected void processMouseEvent(MouseEvent e) {
 314                 return;
 315             }
 316             // Ignore mouse drags, which can also change highlighting
 317             protected void processMouseMotionEvent(MouseEvent e) {
 318                 return;
 319             }
 320         }
 321 
 322         MyList contentsList;
 323         ContentsModel model;
 324         
 325         public WizContentsPanel() {
 326             setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 10));
 327             setBackground(Color.white);
 328             setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
 329 
 330             Mnemonic mnSteps = 
 331                 new Mnemonic(ResourceStrings.getString("steps_label"));
 332             JLabel l = new JLabel(mnSteps.getString());
 333             l.setLabelFor(this);
 334             l.setToolTipText(mnSteps.getString());
 335             l.setDisplayedMnemonic(mnSteps.getMnemonic());
 336 
 337             l.setForeground(Color.black);
 338             l.setAlignmentX(Component.LEFT_ALIGNMENT);
 339             add(l);
 340     
 341             model = new ContentsModel();
 342             contentsList = new MyList(model);
 343             contentsList.setCellRenderer(new ContentsRenderer());
 344             /*
 345              * Wrap the list with scroll bars, vertical as necessary but
 346              * never a horizontal one.
 347              */
 348             JScrollPane scrollPane = new JScrollPane(contentsList,
 349                 ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
 350                 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
 351             scrollPane.setBorder(BorderFactory.createEmptyBorder(15, 10, 0, 0));
 352             scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
 353             scrollPane.setBackground(Color.white);
 354             add(scrollPane);
 355             add(Box.createVerticalGlue());
 356         }
 357         
 358         public void addStep(WizardStep step) {
 359             // Index the steps using the description string
 360             model.addItem(step.getDescription());
 361         }
 362         
 363         public void showStep(WizardStep step) {
 364             contentsList.setSelectedValue(step.getDescription(), true);
 365         }
 366     }
 367 
 368     /*
 369      * This class provides a simple card layout to display each step as needed.
 370      */     
 371     class WizStepPanel extends JPanel {
 372         CardLayout layout;
 373         
 374         public WizStepPanel() {
 375             setLayout(layout = new CardLayout());
 376             // Make sure we have some space around the edges.
 377             setBorder(BorderFactory.createEmptyBorder(15, 10, 10, 10));
 378         }
 379         
 380         public void addStep(WizardStep step) {
 381             add(step.getComponent(), step.getDescription());
 382         }
 383         
 384         public void showStep(WizardStep step) {
 385             layout.show(this, step.getDescription());
 386         }
 387     }
 388     
 389     WizButtonPanel buttonPanel;
 390     WizContentsPanel contentsPanel;
 391     WizStepPanel stepPanel;
 392     Vector steps;
 393     WizardStep activeStep = null;
 394     int stepNumber = 0;
 395     Vector listeners;
 396 
 397     /**
 398      * Constructs a wizard with the specified owning frame and title.
 399      *
 400      * @param   owner   the owning frame
 401      * @param   title   the wizard's title string
 402      */ 
 403     public Wizard(Frame owner, String title) {
 404         super(owner, title);
 405         
 406         setLocationRelativeTo(owner);
 407 
 408         getContentPane().setLayout(new BorderLayout());
 409         steps = new Vector();
 410         listeners = new Vector();
 411         
 412         // Put the buttons at the bottom.
 413         buttonPanel = new WizButtonPanel();
 414         getContentPane().add(buttonPanel, BorderLayout.SOUTH);
 415         
 416         /*
 417          * The main display is designed to use a 7:11 ratio for the horizontal
 418          * area consumed by the contents panel and the display panel,
 419          * respectively.  There's nothing particularly magical about the
 420          * ratio, that's just the way the designer drew it.
 421          */     
 422         JPanel mainPanel = new JPanel(new ProportionalLayout());
 423         
 424         contentsPanel = new WizContentsPanel();
 425         mainPanel.add(contentsPanel, "7");
 426                 
 427         stepPanel = new WizStepPanel();
 428         mainPanel.add(stepPanel, "11");
 429         
 430         // Consume all space not needed for buttons
 431         getContentPane().add(mainPanel, BorderLayout.CENTER);
 432         
 433         /*
 434          * We manage the button interactions, but the steps get limited veto
 435          * power
 436          */
 437         buttonPanel.addWizButtonListener(new WizButtonListener() {
 438             public void buttonPressed(int buttonId) {
 439                 switch (buttonId) {
 440                 case BACK:
 441                     showPreviousStep();
 442                     break;
 443                 case FORWARD:
 444                     showNextStep();
 445                     break;
 446                 case FINISH:
 447                     doFinish();
 448                     break;
 449                 case CANCEL:
 450                     doCancel();
 451                     break;
 452                 case HELP:
 453                     doHelp();
 454                     break;
 455                 default:
 456                 }
 457             }
 458         });
 459     }
 460     
 461     /**
 462      * Override of setVisible to control size when displayed.  Perhaps this
 463      * should be relaxed, but subclasses can always do whatever they want.
 464      * @param state make visible or not?
 465      */
 466     public void setVisible(boolean state) {
 467         if (state) {
 468             setSize(525, 425);
 469         }
 470         super.setVisible(state);
 471     }
 472     
 473     /**
 474      * Adds a step to the wizard.  Steps <bold>must</bold> be added in the
 475      * sequence they will be displayed when traversing forward.
 476      * @param step a <code>WizardStep</code>
 477      */
 478     public void addStep(WizardStep step) {
 479         steps.addElement(step);
 480         contentsPanel.addStep(step);
 481         stepPanel.addStep(step);
 482     }
 483     
 484     private boolean showStep(WizardStep step, int direction) {
 485         // Deactivate currently active step
 486         if (activeStep != null) {
 487             if (!activeStep.setInactive(direction)) {
 488                 // Step vetoed its deactivation.  We honor its wishes.
 489                 return false;
 490             }
 491         }
 492         /*
 493          * Activate new step by updating contents, display area, and possibly
 494          * buttons
 495          */
 496         activeStep = step;
 497         contentsPanel.showStep(step);
 498         stepPanel.showStep(step);
 499         if (step == steps.firstElement()) {
 500             buttonPanel.showFirst();
 501         } else if (step == steps.lastElement()) {
 502             buttonPanel.showLast();
 503         } else {
 504             buttonPanel.showMiddle();
 505         }
 506         activeStep.setActive(direction);
 507         return true;
 508     }
 509     
 510     // Show some arbitrary step indexed by number.
 511     private boolean showStep(int index, int direction)
 512             throws ArrayIndexOutOfBoundsException {
 513         WizardStep ws = (WizardStep)steps.elementAt(index);
 514         return showStep(ws, direction);
 515     }
 516     
 517     /**
 518      * Show the very first step.
 519      */
 520     public void showFirstStep() {
 521         stepNumber = 0;
 522         showStep(stepNumber, WizardStep.FORWARD);
 523     }
 524     
 525     /**
 526      * Show the next step.
 527      */
 528     public void showNextStep() {
 529         ++stepNumber;
 530         try {
 531             // Handle step vetoing deactivation
 532             if (!showStep(stepNumber, WizardStep.FORWARD)) {
 533                 --stepNumber;
 534             }
 535         } catch (ArrayIndexOutOfBoundsException e) {
 536             --stepNumber;
 537         }
 538     }
 539     
 540     /**
 541      * Show the previous step.
 542      */
 543     public void showPreviousStep() {
 544         --stepNumber;
 545         try {
 546             if (!showStep(stepNumber, WizardStep.BACKWARD)) {
 547                 ++stepNumber;
 548             }
 549         } catch (ArrayIndexOutOfBoundsException e) {
 550             ++stepNumber;
 551         }
 552     }
 553     
 554     /**
 555      * Show the last step.
 556      */
 557     public void showLastStep() {
 558         int saveStep = stepNumber;
 559         stepNumber = steps.size()-1;
 560         try {
 561             if (!showStep(stepNumber, WizardStep.FORWARD)) {
 562                 stepNumber = saveStep;
 563             }    
 564         } catch (ArrayIndexOutOfBoundsException e) {
 565             stepNumber = saveStep;
 566         }
 567     }
 568     
 569     /**
 570      * Control state of the forward button.
 571      * @param state <code>true</code> to enable the button
 572      */
 573     public void setForwardEnabled(boolean state) {
 574         buttonPanel.setForwardEnabled(state);
 575     }
 576     
 577     /**
 578      * Control state of the finish button.
 579      * @param state <code>true</code> to enable the button
 580      */
 581     public void setFinishEnabled(boolean state) {
 582         buttonPanel.setFinishEnabled(state);
 583     }
 584     
 585     /**
 586      * Handle user's press of the Cancel button.  Subclasses can override for
 587      * special cleanup needs.
 588      */
 589     public void doCancel() {
 590         fireActionPerformed("cancelled");
 591         dispose();
 592     }
 593     
 594     /**
 595      * Handle user's press of the Finish button.  Subclasses should override
 596      * to perform whatever processing needed to complete the wizard's task.
 597      */
 598     public void doFinish() {
 599         fireActionPerformed("finished");
 600         dispose();
 601     }
 602     
 603     /**
 604      * Handle user's press of the Help button.  Does nothing by default,
 605      * subclasses can override to provide help as desired.
 606      */
 607     public void doHelp() {
 608     }
 609 
 610     /**
 611      * Utility function to create a multi-line text display such as for the
 612      * explanatory text that most wizard steps use.
 613      * @param text the text to display
 614      * @param rows the number of rows to use for displaying the text
 615      * @param columns the number of columns to wrap text at.  45 is generally a
 616      *                  good number for standard wizards with standard fonts
 617      * @return a <code>JComponent</code> displaying the supplied text
 618      */
 619     public static JComponent createTextArea(
 620             String text, int rows, int columns) {
 621 
 622         // We extend JTextArea in order to make this behave more like a label
 623         class MyTextArea extends JTextArea {
 624             public MyTextArea(String text, int rows, int columns) {
 625                 /*
 626                  * Create a text area with word-wrapping, no editing, 
 627                  * and no background.
 628                  */
 629                 super(text, rows, columns);
 630                 setLineWrap(true);
 631                 setWrapStyleWord(true);
 632                 setEditable(false);
 633                 setOpaque(false);
 634                 setFocusable(false);
 635             }
 636         }
 637 
 638         MyTextArea area = new MyTextArea(text, rows, columns);
 639 
 640         // Put it in a scrollpane to get sizing to happen
 641         JScrollPane scrollPane = new JScrollPane(area,
 642             ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER,
 643             ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
 644 
 645         // Put empty borders on the subcomponents so that all we get is text
 646         Border b = BorderFactory.createEmptyBorder();
 647         area.setBorder(b);
 648         scrollPane.setBorder(b);
 649         return scrollPane;
 650     }
 651     
 652     /**
 653      * Add a listener for action events.  Wizard fires an
 654      * <code>ActionEvent</code> when user either cancels or finishes the wizard.
 655      */
 656     public void addActionListener(ActionListener l) {
 657         listeners.addElement(l);
 658     }
 659     
 660     /**
 661      * Remove an action listener.
 662      */
 663     public void removeActionListener(ActionListener l) {
 664         listeners.removeElement(l);
 665     }
 666     
 667     /**
 668      * Fire an action event.
 669      */
 670     protected void fireActionPerformed(String command) {
 671         ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
 672             command);
 673         for (Enumeration en = listeners.elements(); en.hasMoreElements(); ) {
 674             ActionListener l = (ActionListener)en.nextElement();
 675             l.actionPerformed(e);
 676         }
 677     }
 678 }