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 }