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 }