/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Copyright (c) 2013, MPL CodeInside http://codeinside.ru
*/
package ru.codeinside.gses.webui.wizard;
import com.vaadin.terminal.PaintException;
import com.vaadin.terminal.PaintTarget;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Form;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Panel;
import com.vaadin.ui.UriFragmentUtility;
import com.vaadin.ui.UriFragmentUtility.FragmentChangedEvent;
import com.vaadin.ui.UriFragmentUtility.FragmentChangedListener;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window.Notification;
import ru.codeinside.gses.webui.form.DataAccumulator;
import ru.codeinside.gses.webui.wizard.event.WizardCancelledEvent;
import ru.codeinside.gses.webui.wizard.event.WizardCancelledListener;
import ru.codeinside.gses.webui.wizard.event.WizardCompletedEvent;
import ru.codeinside.gses.webui.wizard.event.WizardCompletedListener;
import ru.codeinside.gses.webui.wizard.event.WizardProgressListener;
import ru.codeinside.gses.webui.wizard.event.WizardStepActivationEvent;
import ru.codeinside.gses.webui.wizard.event.WizardStepSetChangedEvent;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Wizard extends CustomComponent implements FragmentChangedListener {
private static final long serialVersionUID = 1L;
protected final List<WizardStep> steps = new ArrayList<WizardStep>();
protected final Map<String, WizardStep> idMap = new HashMap<String, WizardStep>();
protected WizardStep currentStep;
protected WizardStep lastCompletedStep;
private int stepIndex = 1;
protected VerticalLayout mainLayout;
protected HorizontalLayout footer;
private Panel contentPanel;
private Button nextButton;
private Button backButton;
private Button finishButton;
private Button cancelButton;
private Component header;
private UriFragmentUtility uriFragment;
private final DataAccumulator accumulator;
public static final Method WIZARD_ACTIVE_STEP_CHANGED_METHOD;
public static final Method WIZARD_STEP_SET_CHANGED_METHOD;
public static final Method WIZARD_COMPLETED_METHOD;
public static final Method WIZARD_CANCELLED_METHOD;
static {
try {
WIZARD_COMPLETED_METHOD = WizardCompletedListener.class
.getDeclaredMethod("wizardCompleted",
new Class[]{WizardCompletedEvent.class});
WIZARD_STEP_SET_CHANGED_METHOD = WizardProgressListener.class
.getDeclaredMethod("stepSetChanged",
new Class[]{WizardStepSetChangedEvent.class});
WIZARD_ACTIVE_STEP_CHANGED_METHOD = WizardProgressListener.class
.getDeclaredMethod("activeStepChanged",
new Class[]{WizardStepActivationEvent.class});
WIZARD_CANCELLED_METHOD = WizardCancelledListener.class
.getDeclaredMethod("wizardCancelled",
new Class[]{WizardCancelledEvent.class});
} catch (final java.lang.NoSuchMethodException e) {
// This should never happen
throw new java.lang.RuntimeException(
"Internal error finding methods in Wizard", e);
}
}
public Wizard(DataAccumulator accumulator) {
this.accumulator = accumulator;
setStyleName("wizard");
init();
}
private void init() {
mainLayout = new VerticalLayout();
setCompositionRoot(mainLayout);
setSizeFull();
contentPanel = new Panel();
contentPanel.setSizeFull();
initControlButtons();
footer = new HorizontalLayout();
footer.setSpacing(true);
footer.addComponent(cancelButton);
footer.addComponent(backButton);
footer.addComponent(nextButton);
footer.addComponent(finishButton);
mainLayout.addComponent(contentPanel);
mainLayout.addComponent(footer);
mainLayout.setComponentAlignment(footer, Alignment.BOTTOM_RIGHT);
mainLayout.setExpandRatio(contentPanel, 1.0f);
mainLayout.setSizeFull();
initDefaultHeader();
}
private void initControlButtons() {
nextButton = new Button("Далее");
nextButton.addListener(new Button.ClickListener() {
public void buttonClick(ClickEvent event) {
next();
}
});
backButton = new Button("Назад");
backButton.addListener(new Button.ClickListener() {
public void buttonClick(ClickEvent event) {
back();
}
});
finishButton = new Button("Завершить");
finishButton.addListener(new Button.ClickListener() {
public void buttonClick(ClickEvent event) {
finish();
}
});
finishButton.setEnabled(false);
cancelButton = new Button("Отмена");
cancelButton.addListener(new Button.ClickListener() {
public void buttonClick(ClickEvent event) {
cancel();
}
});
}
private void initDefaultHeader() {
WizardProgressBar progressBar = new WizardProgressBar(this);
addListener(progressBar);
setHeader(progressBar);
}
public void setUriFragmentEnabled(boolean enabled) {
if (enabled && uriFragment == null) {
uriFragment = new UriFragmentUtility();
uriFragment.addListener(this);
mainLayout.addComponent(uriFragment);
}
if (uriFragment != null) {
uriFragment.setEnabled(enabled);
}
}
public boolean isUriFragmentEnabled() {
return uriFragment != null && uriFragment.isEnabled();
}
/**
* Sets a {@link Component} that is displayed on top of the actual content.
* Set to {@code null} to remove the header altogether.
*
* @param newHeader {@link Component} to be displayed on top of the actual content
* or {@code null} to remove the header.
*/
public void setHeader(Component newHeader) {
if (header != null) {
if (newHeader == null) {
mainLayout.removeComponent(header);
} else {
mainLayout.replaceComponent(header, newHeader);
}
} else {
if (newHeader != null) {
mainLayout.addComponentAsFirst(newHeader);
}
}
this.header = newHeader;
}
/**
* Returns a {@link Component} that is displayed on top of the actual
* content or {@code null} if no header is specified.
* <p/>
* <p>
* By default the header is a {@link WizardProgressBar} component that is
* also registered as a {@link WizardProgressListener} to this Wizard.
* </p>
*
* @return {@link Component} that is displayed on top of the actual content
* or {@code null}.
*/
public Component getHeader() {
return header;
}
/**
* Adds a step to this Wizard with the given identifier. The used {@code id}
* must be unique or an {@link IllegalArgumentException} is thrown. If you
* don't wish to explicitly provide an identifier, you can use the
* {@link #addStep(WizardStep)} method.
*
* @param step
* @param id
* @throws IllegalStateException if the given {@code id} already exists.
*/
public void addStep(WizardStep step, String id) {
if (idMap.containsKey(id)) {
throw new IllegalArgumentException(
String.format(
"A step with given id %s already exists. You must use unique identifiers for the steps.",
id));
}
steps.add(step);
idMap.put(id, step);
updateButtons();
// notify listeners
fireEvent(new WizardStepSetChangedEvent(this));
}
@Override
public void paintContent(PaintTarget target) throws PaintException {
// make sure there is always a step selected
if (currentStep == null && !steps.isEmpty()) {
// activate the first step
activateStep(steps.get(0));
}
super.paintContent(target);
}
/**
* Adds a step to this Wizard. The WizardStep will be assigned an identifier
* automatically. If you wish to provide an explicit identifier for your
* WizardStep, you can use the {@link #addStep(WizardStep, String)} method
* instead.
*
* @param step
*/
public void addStep(WizardStep step) {
addStep(step, "wizard-step-" + stepIndex++);
}
public void addListener(WizardProgressListener listener) {
addCompletedListener(listener);
addListener(WizardStepActivationEvent.class, listener, WIZARD_ACTIVE_STEP_CHANGED_METHOD);
addListener(WizardStepSetChangedEvent.class, listener, WIZARD_STEP_SET_CHANGED_METHOD);
addCancelledListener(listener);
}
public void removeListener(WizardProgressListener listener) {
removeListener(WizardCompletedEvent.class, listener, WIZARD_COMPLETED_METHOD);
removeListener(WizardStepActivationEvent.class, listener, WIZARD_ACTIVE_STEP_CHANGED_METHOD);
removeListener(WizardStepSetChangedEvent.class, listener, WIZARD_STEP_SET_CHANGED_METHOD);
removeListener(WizardCancelledEvent.class, listener, WIZARD_CANCELLED_METHOD);
}
public void addCancelledListener(WizardCancelledListener listener) {
addListener(WizardCancelledEvent.class, listener, Wizard.WIZARD_CANCELLED_METHOD);
}
public void addCompletedListener(WizardCompletedListener listener) {
addListener(WizardCompletedEvent.class, listener, Wizard.WIZARD_COMPLETED_METHOD);
}
public List<WizardStep> getSteps() {
return Collections.unmodifiableList(steps);
}
/**
* Returns {@code true} if the given step is already completed by the user.
*
* @param step step to check for completion.
* @return {@code true} if the given step is already completed.
*/
public boolean isCompleted(WizardStep step) {
return steps.indexOf(step) < steps.indexOf(currentStep);
}
/**
* Returns {@code true} if the given step is the currently active step.
*
* @param step step to check for.
* @return {@code true} if the given step is the currently active step.
*/
public boolean isActive(WizardStep step) {
return (step == currentStep);
}
private void updateButtons() {
if (isLastStep(currentStep)) {
finishButton.setEnabled(true);
nextButton.setEnabled(false);
} else {
finishButton.setEnabled(false);
nextButton.setEnabled(true);
}
backButton.setEnabled(!isFirstStep(currentStep));
}
public Button getNextButton() {
return nextButton;
}
public Button getBackButton() {
return backButton;
}
public Button getFinishButton() {
return finishButton;
}
public Button getCancelButton() {
return cancelButton;
}
protected void activateStep(WizardStep step) {
if (!allowToChangeStep(step)) {
return;
}
replaceContent(step);
updateUriFragment();
updateButtons();
fireEvent(new WizardStepActivationEvent(this, step));
}
private boolean allowToChangeStep(WizardStep step) {
if (step == null) {
return false;
}
if (currentStep != null) {
if (currentStep.equals(step)) {
// already active
return false;
}
// ask if we're allowed to move
boolean advancing = steps.indexOf(step) > steps.indexOf(currentStep);
if (advancing) {
if (!currentStep.onAdvance()) {
// not allowed to advance
return false;
}
try {
TransitionAction action = step.getTransitionAction();
ResultTransition resultTransition = action.doIt();
step.setResultTransition(resultTransition);
} catch (IllegalStateException e) {
mainLayout.getWindow().showNotification(e.getMessage(), Notification.TYPE_WARNING_MESSAGE);
return false;
}
} else {
if (!currentStep.onBack()) {
// not allowed to go back
return false;
}
currentStep.backwardAction();
}
// keep track of the last step that was completed
int currentIndex = steps.indexOf(currentStep);
if (lastCompletedStep == null || steps.indexOf(lastCompletedStep) < currentIndex) {
lastCompletedStep = currentStep;
}
}
currentStep = step;
return true;
}
private void replaceContent(WizardStep step) {
contentPanel.removeAllComponents();
Component c = step.getContent();
contentPanel.addComponent(c);
if (c instanceof ExpandRequired) {
// требуется всё пространство под содержимиого панели
VerticalLayout vl = (VerticalLayout) contentPanel.getContent();
vl.setSizeFull();
vl.setExpandRatio(c, 1f);
}
if (c instanceof Form) {
accumulator.addForm((Form) c);
}
}
protected void activateStep(String id) {
WizardStep step = idMap.get(id);
if (step != null) {
// check that we don't go past the lastCompletedStep by using the id
int lastCompletedIndex = lastCompletedStep == null ? -1 : steps.indexOf(lastCompletedStep);
int stepIndex = steps.indexOf(step);
if (lastCompletedIndex < stepIndex) {
activateStep(lastCompletedStep);
} else {
activateStep(step);
}
}
}
protected String getId(WizardStep step) {
for (Map.Entry<String, WizardStep> entry : idMap.entrySet()) {
if (entry.getValue().equals(step)) {
return entry.getKey();
}
}
return null;
}
private void updateUriFragment() {
if (isUriFragmentEnabled()) {
String currentStepId = getId(currentStep);
if (currentStepId != null && currentStepId.length() > 0) {
uriFragment.setFragment(currentStepId, false);
} else {
uriFragment.setFragment(null, false);
}
}
}
protected boolean isFirstStep(WizardStep step) {
if (step != null) {
return steps.indexOf(step) == 0;
}
return false;
}
protected boolean isLastStep(WizardStep step) {
if (step != null && !steps.isEmpty()) {
return steps.indexOf(step) == (steps.size() - 1);
}
return false;
}
/**
* Cancels this Wizard triggering a {@link WizardCancelledEvent}. This
* method is called when user clicks the cancel button.
*/
public void cancel() {
fireEvent(new WizardCancelledEvent(this));
}
/**
* Triggers a {@link WizardCompletedEvent} if the current step is the last
* step and it allows advancing (see {@link WizardStep#onAdvance()}). This
* method is called when user clicks the finish button.
*/
public void finish() {
if (isLastStep(currentStep) && currentStep.onAdvance()) {
// next (finish) allowed -> fire complete event
fireEvent(new WizardCompletedEvent(this));
}
}
/**
* Activates the next {@link WizardStep} if the current step allows
* advancing (see {@link WizardStep#onAdvance()}) or calls the
* {@link #finish()} method the current step is the last step. This method
* is called when user clicks the next button.
*/
public void next() {
if (isLastStep(currentStep)) {
finish();
} else {
int currentIndex = steps.indexOf(currentStep);
activateStep(steps.get(currentIndex + 1));
}
}
/**
* Activates the previous {@link WizardStep} if the current step allows
* going back (see {@link WizardStep#onBack()}) and the current step is not
* the first step. This method is called when user clicks the back button.
*/
public void back() {
int currentIndex = steps.indexOf(currentStep);
if (currentIndex > 0) {
activateStep(steps.get(currentIndex - 1));
}
}
public void fragmentChanged(FragmentChangedEvent source) {
if (isUriFragmentEnabled()) {
String fragment = source.getUriFragmentUtility().getFragment();
if (fragment.equals("") && !steps.isEmpty()) {
// empty fragment -> set the fragment of first step
uriFragment.setFragment(getId(steps.get(0)));
} else {
activateStep(fragment);
}
}
}
/**
* Removes the given step from this Wizard. An {@link IllegalStateException}
* is thrown if the given step is already completed or is the currently
* active step.
*
* @param stepToRemove the step to remove.
* @see #isCompleted(WizardStep)
* @see #isActive(WizardStep)
*/
public void removeStep(WizardStep stepToRemove) {
if (idMap.containsValue(stepToRemove)) {
for (Map.Entry<String, WizardStep> entry : idMap.entrySet()) {
if (entry.getValue().equals(stepToRemove)) {
// delegate the actual removal to the overloaded method
removeStep(entry.getKey());
return;
}
}
}
}
/**
* Removes the step with given id from this Wizard. An
* {@link IllegalStateException} is thrown if the given step is already
* completed or is the currently active step.
*
* @param id identifier of the step to remove.
* @see #isCompleted(WizardStep)
* @see #isActive(WizardStep)
*/
public void removeStep(String id) {
if (idMap.containsKey(id)) {
WizardStep stepToRemove = idMap.get(id);
if (isCompleted(stepToRemove)) {
throw new IllegalStateException("Already completed step cannot be removed.");
}
if (isActive(stepToRemove)) {
throw new IllegalStateException("Currently active step cannot be removed.");
}
idMap.remove(id);
steps.remove(stepToRemove);
// notify listeners
fireEvent(new WizardStepSetChangedEvent(this));
}
}
}