blob: 3821a2315ce818c6481fa00bbc2517ea9d67bf1f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Chris Gross (schtoo@schtoo.com) - patch for bug 16179
* Eugene Ostroukhov <eugeneo@symbian.org> - Bug 287887 [Wizards] [api] Cancel button has two distinct roles
* Paul Adams <padams@ittvis.com> - Bug 202534 - [Dialogs] SWT error in Wizard dialog when help is displayed and "Finish" is pressed
* Jan-Ove Weichel <janove.weichel@vogella.com> - Bug 475879
* Willem Sietse Jongman <wim.jongman@remainsoftware.com> - Give Wizards a modality flag
*******************************************************************************/
package org.eclipse.jface.wizard;
import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ControlEnableState;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.dialogs.IPageChangeProvider;
import org.eclipse.jface.dialogs.IPageChangedListener;
import org.eclipse.jface.dialogs.IPageChangingListener;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.PageChangedEvent;
import org.eclipse.jface.dialogs.PageChangingEvent;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.operation.ModalContext;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Shell;
/**
* A dialog to show a wizard to the end user.
* <p>
* In typical usage, the client instantiates this class with a particular
* wizard. The dialog serves as the wizard container and orchestrates the
* presentation of its pages.
* <p>
* The standard layout is roughly as follows: it has an area at the top
* containing both the wizard's title, description, and image; the actual wizard
* page appears in the middle; below that is a progress indicator (which is made
* visible if needed); and at the bottom of the page is message line and a
* button bar containing Help, Next, Back, Finish, and Cancel buttons (or some
* subset).
* </p>
* <p>
* Clients may subclass <code>WizardDialog</code>, although this is rarely
* required.
* </p>
*/
public class WizardDialog extends TitleAreaDialog implements IWizardContainer2, IPageChangeProvider {
/**
* Image registry key for error message image (value
* <code>"dialog_title_error_image"</code>).
*/
public static final String WIZ_IMG_ERROR = "dialog_title_error_image"; //$NON-NLS-1$
// The wizard the dialog is currently showing.
private IWizard wizard;
// Wizards to dispose
private ArrayList<IWizard> createdWizards = new ArrayList<>();
// Current nested wizards
private ArrayList<IWizard> nestedWizards = new ArrayList<>();
// The currently displayed page.
private IWizardPage currentPage = null;
// The number of long running operation executed from the dialog.
private long activeRunningOperations = 0;
/**
* The time in milliseconds where the last job finished. 'Enter' key presses are ignored for the
* next {@link #RESTORE_ENTER_DELAY} milliseconds.
* <p>
* The value <code>-1</code> indicates that the traverse listener needs to be installed.
* </p>
*
* @since 3.6
*/
private long timeWhenLastJobFinished= -1;
// Tells whether a subclass provided the progress monitor part
private boolean useCustomProgressMonitorPart= true;
// The current page message and description
private String pageMessage;
private int pageMessageType = IMessageProvider.NONE;
private String pageDescription;
// The progress monitor
private ProgressMonitorPart progressMonitorPart;
private MessageDialog windowClosingDialog;
// Navigation buttons
private Button backButton;
private Button nextButton;
private Button finishButton;
private Button cancelButton;
private Button helpButton;
private SelectionListener cancelListener;
private boolean isMovingToPreviousPage = false;
private Composite pageContainer;
private PageContainerFillLayout pageContainerLayout = new PageContainerFillLayout(5, 5, 300, 225);
private int pageWidth = SWT.DEFAULT;
private int pageHeight = SWT.DEFAULT;
private static final String FOCUS_CONTROL = "focusControl"; //$NON-NLS-1$
/**
* A delay in milliseconds that reduces the risk that the user accidentally triggers a
* button by pressing the 'Enter' key immediately after a job has finished.
*
* @since 3.6
*/
private static final int RESTORE_ENTER_DELAY= 500;
private boolean lockedUI = false;
private ListenerList<IPageChangedListener> pageChangedListeners = new ListenerList<>();
private ListenerList<IPageChangingListener> pageChangingListeners = new ListenerList<>();
/**
* A layout for a container which includes several pages, like a notebook,
* wizard, or preference dialog. The size computed by this layout is the
* maximum width and height of all pages currently inserted into the
* container.
*/
protected class PageContainerFillLayout extends Layout {
/**
* The margin width; <code>5</code> pixels by default.
*/
public int marginWidth = 5;
/**
* The margin height; <code>5</code> pixels by default.
*/
public int marginHeight = 5;
/**
* The minimum width; <code>0</code> pixels by default.
*/
public int minimumWidth = 0;
/**
* The minimum height; <code>0</code> pixels by default.
*/
public int minimumHeight = 0;
/**
* Creates new layout object.
*
* @param mw
* the margin width
* @param mh
* the margin height
* @param minW
* the minimum width
* @param minH
* the minimum height
*/
public PageContainerFillLayout(int mw, int mh, int minW, int minH) {
marginWidth = mw;
marginHeight = mh;
minimumWidth = minW;
minimumHeight = minH;
}
@Override
public Point computeSize(Composite composite, int wHint, int hHint,
boolean force) {
if (wHint != SWT.DEFAULT && hHint != SWT.DEFAULT) {
return new Point(wHint, hHint);
}
Point result = null;
Control[] children = composite.getChildren();
if (children.length > 0) {
result = new Point(0, 0);
for (Control element : children) {
Point cp = element.computeSize(wHint, hHint, force);
result.x = Math.max(result.x, cp.x);
result.y = Math.max(result.y, cp.y);
}
result.x = result.x + 2 * marginWidth;
result.y = result.y + 2 * marginHeight;
} else {
Rectangle rect = composite.getClientArea();
result = new Point(rect.width, rect.height);
}
result.x = Math.max(result.x, minimumWidth);
result.y = Math.max(result.y, minimumHeight);
if (wHint != SWT.DEFAULT) {
result.x = wHint;
}
if (hHint != SWT.DEFAULT) {
result.y = hHint;
}
return result;
}
/**
* Returns the client area for the given composite according to this
* layout.
*
* @param c
* the composite
* @return the client area rectangle
*/
public Rectangle getClientArea(Composite c) {
Rectangle rect = c.getClientArea();
rect.x = rect.x + marginWidth;
rect.y = rect.y + marginHeight;
rect.width = rect.width - 2 * marginWidth;
rect.height = rect.height - 2 * marginHeight;
return rect;
}
@Override
public void layout(Composite composite, boolean force) {
Rectangle rect = getClientArea(composite);
Control[] children = composite.getChildren();
for (Control element : children) {
element.setBounds(rect);
}
}
/**
* Lays outs the page according to this layout.
*
* @param w
* the control
*/
public void layoutPage(Control w) {
w.setBounds(getClientArea(w.getParent()));
}
/**
* Sets the location of the page so that its origin is in the upper left
* corner.
*
* @param w
* the control
*/
public void setPageLocation(Control w) {
w.setLocation(marginWidth, marginHeight);
}
}
/**
* Creates a new wizard dialog for the given wizard.
*
* @param parentShell
* the parent shell
* @param newWizard
* the wizard this dialog is working on
*/
public WizardDialog(Shell parentShell, IWizard newWizard) {
super(parentShell);
boolean modal = !"true".equals(System.getProperty("jface.allWizardsNonModal", "false")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
setShellStyle(SWT.CLOSE | SWT.MAX | SWT.TITLE | SWT.BORDER | SWT.RESIZE | getShellModality(modal)
| getDefaultOrientation());
setWizard(newWizard);
// since VAJava can't initialize an instance var with an anonymous
// class outside a constructor we do it here:
cancelListener = widgetSelectedAdapter(e -> cancelPressed());
}
/**
* Sets the shell style of the wizard dialog.
* <p>
* Examples:<br>
* To use the default style without the SWT.APPLICATION_MODAL bit:<br>
* <code>setShellStyle(getShellStyle() & ~SWT.APPLICATION_MODAL)</code>
* <p>
* To use the default style without the SWT.RESIZE bit:<br>
* <code>setShellStyle(getShellStyle() & ~SWT.RESIZE)</code>
*
* <p>
* {@inheritDoc}
*
* @see #setModal(boolean)
*
*/
@Override
public void setShellStyle(int newShellStyle) {
super.setShellStyle(newShellStyle);
}
@Override
public int getShellStyle() {
return super.getShellStyle();
}
private static int getShellModality(boolean modal) {
return modal ? SWT.APPLICATION_MODAL : SWT.NONE;
}
/**
* Option to set the modality of the WizardDialog. This method must be called
* before the dialog's shell is created, e.g. before you call {@link #open()}
* for the first time.
*
* @param modal true (default) if the WizardDialog should block the underlying
* window.
* @return this WizardDialog
* @since 3.16
*/
public WizardDialog setModal(boolean modal) {
setShellStyle(getShellStyle() & ~SWT.APPLICATION_MODAL | getShellModality(modal));
return this;
}
/**
* @return <code>false</code> if the user interface blocks the underlying window
* (modal) or <code>true</code> if the underlying window is not blocked.
* @since 3.16
* @see #setModal(boolean)
* @see #setShellStyle(int)
*/
public boolean isModal() {
return (getShellStyle() & SWT.APPLICATION_MODAL) == SWT.APPLICATION_MODAL;
}
/**
* About to start a long running operation triggered through the wizard.
* Shows the progress monitor and disables the wizard's buttons and
* controls.
*
* @param enableCancelButton
* <code>true</code> if the Cancel button should be enabled,
* and <code>false</code> if it should be disabled
* @return the saved UI state
*/
private Map<String, Object> aboutToStart(boolean enableCancelButton) {
Map<String, Object> savedState = null;
if (getShell() != null) {
// Save focus control
Control focusControl = getShell().getDisplay().getFocusControl();
if (focusControl != null && focusControl.getShell() != getShell()) {
focusControl = null;
}
boolean needsProgressMonitor = wizard.needsProgressMonitor();
// Set the busy cursor to all shells.
Display d = getShell().getDisplay();
setDisplayCursor(d.getSystemCursor(SWT.CURSOR_WAIT));
if (useCustomProgressMonitorPart) {
cancelButton.removeSelectionListener(cancelListener);
// Set the arrow cursor to the cancel component.
cancelButton.setCursor(d.getSystemCursor(SWT.CURSOR_ARROW));
}
// Deactivate shell
savedState = saveUIState(useCustomProgressMonitorPart && needsProgressMonitor && enableCancelButton);
if (focusControl != null) {
savedState.put(FOCUS_CONTROL, focusControl);
}
// Activate cancel behavior.
if (needsProgressMonitor) {
if (enableCancelButton || useCustomProgressMonitorPart) {
progressMonitorPart.attachToCancelComponent(cancelButton);
}
progressMonitorPart.setVisible(true);
}
// Install traverse listener once in order to implement 'Enter' and 'Space' key blocking
if (timeWhenLastJobFinished == -1) {
timeWhenLastJobFinished= 0;
getShell().addTraverseListener(e -> {
if (e.detail == SWT.TRAVERSE_RETURN || (e.detail == SWT.TRAVERSE_MNEMONIC && e.keyCode == 32)) {
// We want to ignore the keystroke when we detect that it has been received within the
// delay period after the last operation has finished. This prevents the user from accidentally
// hitting "Enter" or "Space", intending to cancel an operation, but having it processed exactly
// when the operation finished, thus traversing the wizard. If there is another operation still
// running, the UI is locked anyway so we are not in this code. This listener should fire only
// after the UI state is restored (which by definition means all jobs are done.
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=287887
if (timeWhenLastJobFinished != 0 && System.currentTimeMillis() - timeWhenLastJobFinished < RESTORE_ENTER_DELAY) {
e.doit= false;
return;
}
timeWhenLastJobFinished= 0;
}});
}
}
return savedState;
}
/**
* The Back button has been pressed.
*/
protected void backPressed() {
IWizardPage page = currentPage.getPreviousPage();
if (page == null) {
// should never happen since we have already visited the page
return;
}
// set flag to indicate that we are moving back
isMovingToPreviousPage = true;
// show the page
showPage(page);
}
@Override
protected void buttonPressed(int buttonId) {
switch (buttonId) {
case IDialogConstants.HELP_ID: {
helpPressed();
break;
}
case IDialogConstants.BACK_ID: {
backPressed();
break;
}
case IDialogConstants.NEXT_ID: {
nextPressed();
break;
}
case IDialogConstants.FINISH_ID: {
finishPressed();
break;
}
// The Cancel button has a listener which calls cancelPressed
// directly
}
}
/**
* Calculates the difference in size between the given page and the page
* container. A larger page results in a positive delta.
*
* @param page
* the page
* @return the size difference encoded as a
* <code>new Point(deltaWidth,deltaHeight)</code>
*/
private Point calculatePageSizeDelta(IWizardPage page) {
Control pageControl = page.getControl();
if (pageControl == null) {
// control not created yet
return new Point(0, 0);
}
Point contentSize = pageControl.computeSize(SWT.DEFAULT, SWT.DEFAULT,
true);
Rectangle rect = pageContainerLayout.getClientArea(pageContainer);
Point containerSize = new Point(rect.width, rect.height);
return new Point(Math.max(0, contentSize.x - containerSize.x), Math
.max(0, contentSize.y - containerSize.y));
}
@Override
protected void cancelPressed() {
if (activeRunningOperations <= 0) {
// Close the dialog. The check whether the dialog can be
// closed or not is done in <code>okToClose</code>.
// This ensures that the check is also evaluated when the user
// presses the window's close button.
setReturnCode(CANCEL);
close();
} else {
cancelButton.setEnabled(false);
}
}
@Override
public boolean close() {
if (okToClose()) {
return hardClose();
}
return false;
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
// Register help listener on the shell
newShell.addHelpListener(event -> {
// call perform help on the current page
if (currentPage != null) {
currentPage.performHelp();
}
});
}
/**
* Creates the buttons for this dialog's button bar.
* <p>
* The <code>WizardDialog</code> implementation of this framework method
* prevents the parent composite's columns from being made equal width in
* order to remove the margin between the Back and Next buttons.
* </p>
*
* @param parent
* the parent composite to contain the buttons
*/
@Override
protected void createButtonsForButtonBar(Composite parent) {
((GridLayout) parent.getLayout()).makeColumnsEqualWidth = false;
if (wizard.isHelpAvailable()) {
helpButton = createButton(parent, IDialogConstants.HELP_ID, IDialogConstants.HELP_LABEL, false);
}
if (wizard.needsPreviousAndNextButtons()) {
createPreviousAndNextButtons(parent);
}
finishButton = createButton(parent, IDialogConstants.FINISH_ID, IDialogConstants.FINISH_LABEL, true);
cancelButton = createCancelButton(parent);
if (parent.getDisplay().getDismissalAlignment() == SWT.RIGHT) {
// Make the default button the right-most button.
// See also special code in org.eclipse.jface.dialogs.Dialog#initializeBounds()
finishButton.moveBelow(null);
}
}
@Override
protected void setButtonLayoutData(Button button) {
GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
// On large fonts this can make this dialog huge
widthHint = Math.min(widthHint,
button.getDisplay().getBounds().width / 5);
Point minSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
data.widthHint = Math.max(widthHint, minSize.x);
button.setLayoutData(data);
}
/**
* Creates the Cancel button for this wizard dialog. Creates a standard (<code>SWT.PUSH</code>)
* button and registers for its selection events. Note that the number of
* columns in the button bar composite is incremented. The Cancel button is
* created specially to give it a removeable listener.
*
* @param parent
* the parent button bar
* @return the new Cancel button
*/
private Button createCancelButton(Composite parent) {
// increment the number of columns in the button bar
((GridLayout) parent.getLayout()).numColumns++;
Button button = new Button(parent, SWT.PUSH);
button.setText(IDialogConstants.CANCEL_LABEL);
setButtonLayoutData(button);
button.setFont(parent.getFont());
button.setData(Integer.valueOf(IDialogConstants.CANCEL_ID));
button.addSelectionListener(cancelListener);
return button;
}
/**
* Return the cancel button if the id is a the cancel id.
*
* @param id
* the button id
* @return the button corresponding to the button id
*/
@Override
protected Button getButton(int id) {
if (id == IDialogConstants.CANCEL_ID) {
return cancelButton;
}
return super.getButton(id);
}
/**
* The <code>WizardDialog</code> implementation of this
* <code>Window</code> method calls call <code>IWizard.addPages</code>
* to allow the current wizard to add extra pages, then
* <code>super.createContents</code> to create the controls. It then calls
* <code>IWizard.createPageControls</code> to allow the wizard to
* pre-create their page controls prior to opening, so that the wizard opens
* to the correct size. And finally it shows the first page.
*/
@Override
protected Control createContents(Composite parent) {
// Allow the wizard to add pages to itself
// Need to call this now so page count is correct
// for determining if next/previous buttons are needed
wizard.addPages();
Control contents = super.createContents(parent);
// Allow the wizard pages to precreate their page controls
createPageControls();
// Show the first page
showStartingPage();
return contents;
}
@Override
protected Control createDialogArea(Composite parent) {
Composite composite = (Composite) super.createDialogArea(parent);
// Build the Page container
pageContainer = createPageContainer(composite);
GridData gd = new GridData(GridData.FILL_BOTH);
gd.widthHint = pageWidth;
gd.heightHint = pageHeight;
pageContainer.setLayoutData(gd);
pageContainer.setFont(parent.getFont());
// Insert a progress monitor
progressMonitorPart= createProgressMonitorPart(composite, new GridLayout());
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
if (!wizard.needsProgressMonitor()) {
gridData.exclude = true;
}
progressMonitorPart.setLayoutData(gridData);
progressMonitorPart.setVisible(false);
// Build the separator line
Label separator = new Label(composite, SWT.HORIZONTAL | SWT.SEPARATOR);
separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
applyDialogFont(progressMonitorPart);
return composite;
}
/**
* Hook method for subclasses to create a custom progress monitor part.
* <p>
* The default implementation creates a progress monitor with a stop button will be created.
* </p>
*
* @param composite the parent composite
* @param pmlayout the layout
* @return ProgressMonitorPart the progress monitor part
*/
protected ProgressMonitorPart createProgressMonitorPart(
Composite composite, GridLayout pmlayout) {
useCustomProgressMonitorPart= false;
return new ProgressMonitorPart(composite, pmlayout, true) {
String currentTask = null;
@Override
public void setBlocked(IStatus reason) {
super.setBlocked(reason);
if (!lockedUI) {
getBlockedHandler().showBlocked(getShell(), this, reason,
currentTask);
}
}
@Override
public void clearBlocked() {
super.clearBlocked();
if (!lockedUI) {
getBlockedHandler().clearBlocked();
}
}
@Override
public void beginTask(String name, int totalWork) {
super.beginTask(name, totalWork);
currentTask = name;
}
@Override
public void setTaskName(String name) {
super.setTaskName(name);
currentTask = name;
}
@Override
public void subTask(String name) {
super.subTask(name);
// If we haven't got anything yet use this value for more
// context
if (currentTask == null) {
currentTask = name;
}
}
};
}
/**
* Creates the container that holds all pages.
*
* @param parent
* @return Composite
*/
private Composite createPageContainer(Composite parent) {
Composite result = new Composite(parent, SWT.NULL);
result.setLayout(pageContainerLayout);
return result;
}
/**
* Allow the wizard's pages to pre-create their page controls. This allows
* the wizard dialog to open to the correct size.
*/
private void createPageControls() {
// Allow the wizard pages to precreate their page controls
// This allows the wizard to open to the correct size
wizard.createPageControls(pageContainer);
// Ensure that all of the created pages are initially not visible
IWizardPage[] pages = wizard.getPages();
for (IWizardPage page : pages) {
if (page.getControl() != null) {
page.getControl().setVisible(false);
}
}
}
/**
* Creates the Previous and Next buttons for this wizard dialog. Creates
* standard (<code>SWT.PUSH</code>) buttons and registers for their
* selection events. Note that the number of columns in the button bar
* composite is incremented. These buttons are created specially to prevent
* any space between them.
*
* @param parent
* the parent button bar
* @return a composite containing the new buttons
*/
private Composite createPreviousAndNextButtons(Composite parent) {
// increment the number of columns in the button bar
((GridLayout) parent.getLayout()).numColumns++;
Composite composite = new Composite(parent, SWT.NONE);
// create a layout with spacing and margins appropriate for the font
// size.
GridLayout layout = new GridLayout();
layout.numColumns = 0; // will be incremented by createButton
layout.marginWidth = 0;
layout.marginHeight = 0;
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
composite.setLayout(layout);
GridData data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER | GridData.VERTICAL_ALIGN_CENTER);
composite.setLayoutData(data);
composite.setFont(parent.getFont());
backButton = createButton(composite, IDialogConstants.BACK_ID, IDialogConstants.BACK_LABEL, false);
nextButton = createButton(composite, IDialogConstants.NEXT_ID, IDialogConstants.NEXT_LABEL, false);
// make sure screen readers skip visual '<', '>' chars on buttons:
final String backReaderText = IDialogConstants.BACK_LABEL.replace('<', ' ');
backButton.getAccessible().addAccessibleListener(new AccessibleAdapter() {
@Override
public void getName(AccessibleEvent e) {
e.result = backReaderText;
}
});
final String nextReaderText = IDialogConstants.NEXT_LABEL.replace('>', ' ');
nextButton.getAccessible().addAccessibleListener(new AccessibleAdapter() {
@Override
public void getName(AccessibleEvent e) {
e.result = nextReaderText;
}
});
return composite;
}
/**
* Creates and return a new wizard closing dialog without opening it.
*
* @return MessageDalog
*/
private MessageDialog createWizardClosingDialog() {
MessageDialog result = new MessageDialog(getShell(),
JFaceResources.getString("WizardClosingDialog.title"), //$NON-NLS-1$
null,
JFaceResources.getString("WizardClosingDialog.message"), //$NON-NLS-1$
MessageDialog.QUESTION,
0, IDialogConstants.OK_LABEL) {
@Override
protected int getShellStyle() {
return super.getShellStyle() | SWT.SHEET;
}
};
return result;
}
/**
* The Finish button has been pressed.
*/
protected void finishPressed() {
// Wizards are added to the nested wizards list in setWizard.
// This means that the current wizard is always the last wizard in the
// list.
// Note that we first call the current wizard directly (to give it a
// chance to
// abort, do work, and save state) then call the remaining n-1 wizards
// in the
// list (to save state).
if (wizard.performFinish()) {
// Call perform finish on outer wizards in the nested chain
// (to allow them to save state for example)
for (int i = 0; i < nestedWizards.size() - 1; i++) {
nestedWizards.get(i).performFinish();
}
// Hard close the dialog.
setReturnCode(OK);
hardClose();
}
}
@Override
public IWizardPage getCurrentPage() {
return currentPage;
}
/**
* Returns the progress monitor for this wizard dialog (if it has one).
*
* @return the progress monitor, or <code>null</code> if this wizard
* dialog does not have one
*/
protected IProgressMonitor getProgressMonitor() {
return progressMonitorPart;
}
/**
* Returns the wizard this dialog is currently displaying.
*
* @return the current wizard
*/
protected IWizard getWizard() {
return wizard;
}
/**
* Closes this window.
*
* @return <code>true</code> if the window is (or was already) closed, and
* <code>false</code> if it is still open
*/
private boolean hardClose() {
// inform wizards
for (int i = 0; i < createdWizards.size(); i++) {
IWizard createdWizard = createdWizards.get(i);
try {
createdWizard.dispose();
} catch (Exception e) {
Status status = new Status(IStatus.ERROR, Policy.JFACE, IStatus.ERROR, e.getMessage(), e);
Policy.getLog().log(status);
}
// Remove this dialog as a parent from the managed wizard.
// Note that we do this after calling dispose as the wizard or
// its pages may need access to the container during
// dispose code
createdWizard.setContainer(null);
}
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=202534
// disposing the wizards could cause the image currently set in
// this dialog to be disposed. A subsequent repaint event during
// close would then fail. To prevent this case, we null out the image.
setTitleImage(null);
return super.close();
}
/**
* The Help button has been pressed.
*/
protected void helpPressed() {
if (currentPage != null) {
currentPage.performHelp();
}
}
/**
* The Next button has been pressed.
*/
protected void nextPressed() {
IWizardPage page = currentPage.getNextPage();
if (page == null) {
// something must have happened getting the next page
return;
}
// show the next page
showPage(page);
}
/**
* Notifies page changing listeners and returns result of page changing
* processing to the sender.
*
* @param eventType
* @return <code>true</code> if page changing listener completes
* successfully, <code>false</code> otherwise
*/
private boolean doPageChanging(IWizardPage targetPage) {
PageChangingEvent e = new PageChangingEvent(this, getCurrentPage(), targetPage);
firePageChanging(e);
// Prevent navigation if necessary
return e.doit;
}
/**
* Checks whether it is alright to close this wizard dialog and performed
* standard cancel processing. If there is a long running operation in
* progress, this method posts an alert message saying that the wizard
* cannot be closed.
*
* @return <code>true</code> if it is alright to close this dialog, and
* <code>false</code> if it is not
*/
private boolean okToClose() {
if (activeRunningOperations > 0) {
synchronized (this) {
windowClosingDialog = createWizardClosingDialog();
}
windowClosingDialog.open();
synchronized (this) {
windowClosingDialog = null;
}
return false;
}
return wizard.performCancel();
}
/**
* Restores the enabled/disabled state of the given control.
*
* @param control
* the control
* @param saveState
* a map containing the enabled/disabled state of the wizard dialog's buttons
* @param key
* the key
* @see #saveEnableStateAndSet
*/
private void restoreEnableState(Control control, Map<String,Object> saveState, String key) {
if (control != null) {
Boolean b = (Boolean) saveState.get(key);
if (b != null) {
control.setEnabled(b.booleanValue());
}
}
}
/**
* Restores the enabled/disabled state of the wizard dialog's buttons and
* the tree of controls for the currently showing page.
*
* @param saveState
* a map containing the saved state as returned by
* <code>saveUIState</code>
* @see #saveUIState
*/
private void restoreUIState(Map<String, Object> saveState) {
restoreEnableState(backButton, saveState, "back"); //$NON-NLS-1$
restoreEnableState(nextButton, saveState, "next"); //$NON-NLS-1$
restoreEnableState(finishButton, saveState, "finish"); //$NON-NLS-1$
restoreEnableState(cancelButton, saveState, "cancel"); //$NON-NLS-1$
restoreEnableState(helpButton, saveState, "help"); //$NON-NLS-1$
Object pageValue = saveState.get("page"); //$NON-NLS-1$
if (pageValue != null) {
((ControlEnableState) pageValue).restore();
}
}
/**
* This implementation of IRunnableContext#run(boolean, boolean,
* IRunnableWithProgress) blocks until the runnable has been run, regardless
* of the value of <code>fork</code>. It is recommended that
* <code>fork</code> is set to true in most cases. If <code>fork</code>
* is set to <code>false</code>, the runnable will run in the UI thread
* and it is the runnable's responsibility to call
* <code>Display.readAndDispatch()</code> to ensure UI responsiveness.
*
* UI state is saved prior to executing the long-running operation and is
* restored after the long-running operation completes executing. Any
* attempt to change the UI state of the wizard in the long-running
* operation will be nullified when original UI state is restored.
*
*/
@Override
public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable)
throws InvocationTargetException,
InterruptedException {
// The operation can only be canceled if it is executed in a separate
// thread.
// Otherwise the UI is blocked anyway.
Map<String, Object> state = null;
if (activeRunningOperations++ == 0) {
state = aboutToStart(fork && cancelable);
}
IProgressMonitor progressMonitor = getProgressMonitor();
if (progressMonitor == null) {
progressMonitor = new NullProgressMonitor();
}
try {
if (!fork) {
lockedUI = true;
}
ModalContext.run(runnable, fork, progressMonitor, getShell()
.getDisplay());
lockedUI = false;
} finally {
// explicitly invoke done() on our progress monitor so that its
// label does not spill over to the next invocation, see bug 271530
progressMonitor.done();
// Stop if this is the last one
if (state != null) {
timeWhenLastJobFinished= System.currentTimeMillis();
stopped(state);
}
activeRunningOperations--;
}
}
/**
* Saves the enabled/disabled state of the given control in the given map,
* which must be modifiable.
*
* @param control
* the control, or <code>null</code> if none
* @param saveState
* a map containing the enabled/disabled state of the wizard dialog's buttons
* @param key
* the key
* @param enabled
* <code>true</code> to enable the control, and
* <code>false</code> to disable it
* @see #restoreEnableState(Control, Map, String)
*/
private void saveEnableStateAndSet(Control control, Map<String, Object> saveState, String key, boolean enabled) {
if (control != null) {
saveState.put(key, control.getEnabled() ? Boolean.TRUE : Boolean.FALSE);
control.setEnabled(enabled);
}
}
/**
* Captures and returns the enabled/disabled state of the wizard dialog's
* buttons and the tree of controls for the currently showing page. All
* these controls are disabled in the process, with the possible exception
* of the Cancel button.
*
* @param keepCancelEnabled
* <code>true</code> if the Cancel button should remain
* enabled, and <code>false</code> if it should be disabled
* @return a map containing the saved state suitable for restoring later
* with <code>restoreUIState</code>
* @see #restoreUIState
*/
private Map<String, Object> saveUIState(boolean keepCancelEnabled) {
Map<String, Object> savedState = new HashMap<>(10);
saveEnableStateAndSet(backButton, savedState, "back", false); //$NON-NLS-1$
saveEnableStateAndSet(nextButton, savedState, "next", false); //$NON-NLS-1$
saveEnableStateAndSet(finishButton, savedState, "finish", false); //$NON-NLS-1$
saveEnableStateAndSet(cancelButton, savedState, "cancel", keepCancelEnabled); //$NON-NLS-1$
saveEnableStateAndSet(helpButton, savedState, "help", false); //$NON-NLS-1$
if (currentPage != null) {
savedState.put("page", ControlEnableState.disable(currentPage.getControl())); //$NON-NLS-1$
}
return savedState;
}
/**
* Sets the given cursor for all shells currently active for this window's
* display.
*
* @param c
* the cursor
*/
private void setDisplayCursor(Cursor c) {
Shell[] shells = getShell().getDisplay().getShells();
for (Shell shell : shells) {
shell.setCursor(c);
}
}
/**
* Sets the minimum page size used for the pages.
*
* @param minWidth
* the minimum page width
* @param minHeight
* the minimum page height
* @see #setMinimumPageSize(Point)
*/
public void setMinimumPageSize(int minWidth, int minHeight) {
Assert.isTrue(minWidth >= 0 && minHeight >= 0);
pageContainerLayout.minimumWidth = minWidth;
pageContainerLayout.minimumHeight = minHeight;
}
/**
* Sets the minimum page size used for the pages.
*
* @param size
* the page size encoded as <code>new Point(width,height)</code>
* @see #setMinimumPageSize(int,int)
*/
public void setMinimumPageSize(Point size) {
setMinimumPageSize(size.x, size.y);
}
/**
* Sets the size of all pages. The given size takes precedence over computed
* sizes.
*
* @param width
* the page width
* @param height
* the page height
* @see #setPageSize(Point)
*/
public void setPageSize(int width, int height) {
pageWidth = width;
pageHeight = height;
}
/**
* Sets the size of all pages. The given size takes precedence over computed
* sizes.
*
* @param size
* the page size encoded as <code>new Point(width,height)</code>
* @see #setPageSize(int,int)
*/
public void setPageSize(Point size) {
setPageSize(size.x, size.y);
}
/**
* Sets the wizard this dialog is currently displaying.
*
* @param newWizard
* the wizard
*/
protected void setWizard(IWizard newWizard) {
wizard = newWizard;
wizard.setContainer(this);
if (!createdWizards.contains(wizard)) {
createdWizards.add(wizard);
// New wizard so just add it to the end of our nested list
nestedWizards.add(wizard);
if (pageContainer != null) {
// Dialog is already open
// Allow the wizard pages to precreate their page controls
// This allows the wizard to open to the correct size
createPageControls();
// Ensure the dialog is large enough for the wizard
updateSizeForWizard(wizard);
pageContainer.layout(true);
}
} else {
// We have already seen this wizard, if it is the previous wizard
// on the nested list then we assume we have gone back and remove
// the last wizard from the list
int size = nestedWizards.size();
if (size >= 2 && nestedWizards.get(size - 2) == wizard) {
nestedWizards.remove(size - 1);
} else {
// Assume we are going forward to revisit a wizard
nestedWizards.add(wizard);
}
}
}
@Override
public void showPage(IWizardPage page) {
if (page == null || page == currentPage) {
return;
}
if (!isMovingToPreviousPage) {
// remember my previous page.
page.setPreviousPage(currentPage);
} else {
isMovingToPreviousPage = false;
}
// If page changing evaluation unsuccessful, do not change the page
if (!doPageChanging(page))
return;
// Update for the new page in a busy cursor if possible
if (getContents() == null) {
updateForPage(page);
} else {
final IWizardPage finalPage = page;
BusyIndicator.showWhile(getContents().getDisplay(), () -> updateForPage(finalPage));
}
}
/**
* Update the receiver for the new page.
*
* @param page
*/
private void updateForPage(IWizardPage page) {
// ensure this page belongs to the current wizard
if (wizard != page.getWizard()) {
setWizard(page.getWizard());
}
// ensure that page control has been created
// (this allows lazy page control creation)
if (page.getControl() == null) {
page.createControl(pageContainer);
// the page is responsible for ensuring the created control is
// accessible via getControl.
Assert.isNotNull(page.getControl(),
JFaceResources.format(JFaceResources.getString("WizardDialog.missingSetControl"), //$NON-NLS-1$
page.getName()));
// ensure the dialog is large enough for this page
updateSize(page);
}
// make the new page visible
IWizardPage oldPage = currentPage;
currentPage = page;
currentPage.setVisible(true);
if (oldPage != null) {
oldPage.setVisible(false);
}
// update the dialog controls
update();
}
/**
* Shows the starting page of the wizard.
*/
private void showStartingPage() {
currentPage = wizard.getStartingPage();
if (currentPage == null) {
// something must have happened getting the page
return;
}
// ensure the page control has been created
if (currentPage.getControl() == null) {
currentPage.createControl(pageContainer);
// the page is responsible for ensuring the created control is
// accessible via getControl.
Assert.isNotNull(currentPage.getControl());
// we do not need to update the size since the call
// to initialize bounds has not been made yet.
}
// make the new page visible
currentPage.setVisible(true);
// update the dialog controls
update();
}
/**
* A long running operation triggered through the wizard was stopped either
* by user input or by normal end. Hides the progress monitor and restores
* the enable state wizard's buttons and controls.
*
* @param savedState
* the saved UI state as returned by <code>aboutToStart</code>
* @see #aboutToStart
*/
private void stopped(Map<String, Object> savedState) {
if (getShell() != null && !getShell().isDisposed()) {
if (wizard.needsProgressMonitor()) {
progressMonitorPart.setVisible(false);
progressMonitorPart.removeFromCancelComponent(cancelButton);
}
restoreUIState(savedState);
setDisplayCursor(null);
if (useCustomProgressMonitorPart) {
cancelButton.addSelectionListener(cancelListener);
cancelButton.setCursor(null);
}
Control focusControl = (Control) savedState.get(FOCUS_CONTROL);
if (focusControl != null && !focusControl.isDisposed()) {
focusControl.setFocus();
}
}
}
/**
* Updates this dialog's controls to reflect the current page.
*/
protected void update() {
// Update the window title
updateWindowTitle();
// Update the title bar
updateTitleBar();
// Update the buttons
updateButtons();
// Fires the page change event
firePageChanged(new PageChangedEvent(this, getCurrentPage()));
}
@Override
public void updateButtons() {
boolean canFlipToNextPage = false;
boolean canFinish = wizard.canFinish();
if (backButton != null) {
boolean backEnabled = currentPage != null && currentPage.getPreviousPage() != null;
backButton.setEnabled(backEnabled);
}
if (nextButton != null) {
canFlipToNextPage = currentPage != null && currentPage.canFlipToNextPage();
nextButton.setEnabled(canFlipToNextPage);
}
finishButton.setEnabled(canFinish);
// finish is default unless it is disabled and next is enabled
if (canFlipToNextPage && !canFinish) {
getShell().setDefaultButton(nextButton);
} else {
getShell().setDefaultButton(finishButton);
}
}
/**
* Update the message line with the page's description.
* <p>
* A description is shown only if there is no message or error message.
* </p>
*/
private void updateDescriptionMessage() {
pageDescription = currentPage.getDescription();
setMessage(pageDescription);
}
@Override
public void updateMessage() {
if (currentPage == null) {
return;
}
pageMessage = currentPage.getMessage();
if (pageMessage != null && currentPage instanceof IMessageProvider) {
pageMessageType = ((IMessageProvider) currentPage).getMessageType();
} else {
pageMessageType = IMessageProvider.NONE;
}
if (pageMessage == null) {
setMessage(pageDescription);
} else {
setMessage(pageMessage, pageMessageType);
}
setErrorMessage(currentPage.getErrorMessage());
}
/**
* Changes the shell size to the given size, ensuring that it is no larger
* than the display bounds.
*
* @param width
* the shell width
* @param height
* the shell height
*/
private void setShellSize(int width, int height) {
Rectangle size = getShell().getBounds();
size.height = height;
size.width = width;
getShell().setBounds(getConstrainedShellBounds(size));
}
/**
* Computes the correct dialog size for the current page and resizes its shell if necessary.
* Also causes the container to refresh its layout.
*
* @param page the wizard page to use to resize the dialog
* @since 2.0
*/
protected void updateSize(IWizardPage page) {
if (page == null || page.getControl() == null) {
return;
}
updateSizeForPage(page);
pageContainerLayout.layoutPage(page.getControl());
}
@Override
public void updateSize() {
updateSize(currentPage);
}
/**
* Computes the correct dialog size for the given page and resizes its shell if necessary.
*
* @param page the wizard page
*/
private void updateSizeForPage(IWizardPage page) {
// ensure the page container is large enough
Point delta = calculatePageSizeDelta(page);
if (delta.x > 0 || delta.y > 0) {
// increase the size of the shell
Shell shell = getShell();
Point shellSize = shell.getSize();
setShellSize(shellSize.x + delta.x, shellSize.y + delta.y);
constrainShellSize();
}
}
/**
* Computes the correct dialog size for the given wizard and resizes its shell if necessary.
*
* @param sizingWizard the wizard
*/
private void updateSizeForWizard(IWizard sizingWizard) {
Point delta = new Point(0, 0);
IWizardPage[] pages = sizingWizard.getPages();
for (IWizardPage page : pages) {
// ensure the page container is large enough
Point pageDelta = calculatePageSizeDelta(page);
delta.x = Math.max(delta.x, pageDelta.x);
delta.y = Math.max(delta.y, pageDelta.y);
}
if (delta.x > 0 || delta.y > 0) {
// increase the size of the shell
Shell shell = getShell();
Point shellSize = shell.getSize();
setShellSize(shellSize.x + delta.x, shellSize.y + delta.y);
}
}
@Override
public void updateTitleBar() {
String s = null;
if (currentPage != null) {
s = currentPage.getTitle();
}
if (s == null) {
s = ""; //$NON-NLS-1$
}
setTitle(s);
if (currentPage != null) {
setTitleImage(currentPage.getImage());
updateDescriptionMessage();
}
updateMessage();
}
@Override
public void updateWindowTitle() {
if (getShell() == null) {
// Not created yet
return;
}
String title = wizard.getWindowTitle();
if (title == null) {
title = ""; //$NON-NLS-1$
}
getShell().setText(title);
}
@Override
public Object getSelectedPage() {
return getCurrentPage();
}
@Override
public void addPageChangedListener(IPageChangedListener listener) {
pageChangedListeners.add(listener);
}
@Override
public void removePageChangedListener(IPageChangedListener listener) {
pageChangedListeners.remove(listener);
}
/**
* Notifies any selection changed listeners that the selected page has
* changed. Only listeners registered at the time this method is called are
* notified.
*
* @param event
* a selection changed event
*
* @see IPageChangedListener#pageChanged
*
* @since 3.1
*/
protected void firePageChanged(final PageChangedEvent event) {
for (IPageChangedListener l : pageChangedListeners) {
SafeRunnable.run(new SafeRunnable() {
@Override
public void run() {
l.pageChanged(event);
}
});
}
}
/**
* Adds a listener for page changes to the list of page changing listeners
* registered for this dialog. Has no effect if an identical listener is
* already registered.
*
* @param listener
* a page changing listener
* @since 3.3
*/
public void addPageChangingListener(IPageChangingListener listener) {
pageChangingListeners.add(listener);
}
/**
* Removes the provided page changing listener from the list of page
* changing listeners registered for the dialog.
*
* @param listener
* a page changing listener
* @since 3.3
*/
public void removePageChangingListener(IPageChangingListener listener) {
pageChangingListeners.remove(listener);
}
/**
* Notifies any page changing listeners that the currently selected dialog
* page is changing. Only listeners registered at the time this method is
* called are notified.
*
* @param event
* a selection changing event
*
* @see IPageChangingListener#handlePageChanging(PageChangingEvent)
* @since 3.3
*/
protected void firePageChanging(final PageChangingEvent event) {
for (IPageChangingListener l : pageChangingListeners) {
SafeRunnable.run(new SafeRunnable() {
@Override
public void run() {
l.handlePageChanging(event);
}
});
}
}
}