ru.codeinside.gses.webui.wizard.Wizard.java Source code

Java tutorial

Introduction

Here is the source code for ru.codeinside.gses.webui.wizard.Wizard.java

Source

/*
 * 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));
        }
    }

}