com.android.tools.idea.wizard.model.ModelWizard.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.wizard.model.ModelWizard.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.wizard.model;

import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.ui.properties.BindingsManager;
import com.android.tools.idea.ui.properties.core.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A wizard that owns a series of {@link ModelWizardStep}s. When finished, it iterates through its
 * steps, queries the {@link WizardModel} they're associated with, and calls their
 * {@link WizardModel#handleFinished()} method.
 * <p/>
 * In this way, users of this framework can design steps which handle the UI logic while putting
 * all non-UI business logic in a data model class.
 *
 * To avoid memory leaks, you must dispose a wizard when you're done with it (although this may be
 * done for you by a wrapping class, such as a model wizard dialog).
 */
public final class ModelWizard implements Disposable {

    private final List<ModelWizardStep> mySteps;

    /**
     * When we check if we should show a step, we also check the step's ancestor chain, and make sure
     * all of those should be shown as well. In this way, skipping a parent step automatically will
     * skip any child steps as well (recursively).
     */
    private final Map<ModelWizardStep, ModelWizardStep> myStepOwners = Maps.newHashMap();

    private final BindingsManager myBindings = new BindingsManager();
    private final BoolProperty myCanGoBack = new BoolValueProperty();
    private final BoolProperty myCanGoForward = new BoolValueProperty();
    private final BoolProperty myOnLastStep = new BoolValueProperty();
    private final OptionalProperty<Action> myExtraAction = new OptionalValueProperty<>();

    private final Stack<ModelWizardStep> myPrevSteps = new Stack<>();

    private final StringProperty myTitle = new StringValueProperty();
    private final JPanel myContentPanel = new JPanel(new CardLayout());

    private final List<ResultListener> myResultListeners = Lists.newArrayListWithExpectedSize(1);

    private int myCurrIndex = -1;

    /**
     * Construct a wizard with all of the steps it can potentially show (although some may be
     * hidden). If a step provides its own dependent steps, those, too, will be automatically added
     * at this time.
     * <p/>
     * A wizard, once constructed, is ready to go and will already be pointing at the first step. The
     * next expected action is for {@link #goForward()} to be called. Most likely, a wrapping UI
     * container, such as {@link ModelWizardDialog}, will be responsible for handling this.
     * <p/>
     * When the wizard is finished, it will iterate through its steps, in order, and run
     * {@link WizardModel#handleFinished()} on each of their associated models.
     * <p/>
     * Note: You don't use this constructor directly - instead, use {@link Builder#build()}.
     *
     * @throws IllegalArgumentException if {@code steps} is empty or none of the steps are visible.
     */
    private ModelWizard(@NotNull Collection<ModelWizardStep> steps) {
        mySteps = Lists.newArrayListWithExpectedSize(steps.size());
        for (ModelWizardStep step : steps) {
            addStep(step);
        }

        if (mySteps.isEmpty()) {
            throw new IllegalStateException("Can't create a wizard with no steps");
        }

        myCanGoForward.addListener(sender -> {
            if (myCanGoForward.get()) {
                // Make double sure that, when we switch from blocked to can proceed, we check that no
                // future steps also became visible or hidden at some point. Otherwise, we might think
                // we're on the last step when we're not (or vice versa).
                myOnLastStep.set(isOnLastVisibleStep());
            }
        });

        Set<WizardModel> seenModels = Sets.newHashSet();
        for (ModelWizardStep step : mySteps) {
            Disposer.register(this, step);

            WizardModel model = step.getModel();
            if (seenModels.contains(model)) {
                continue;
            }
            Disposer.register(this, model);
            seenModels.add(model);
        }

        // At this point, we're ready to go! Try to start the wizard, proceeding into the first step
        // if we can.

        Facade facade = new Facade();
        for (ModelWizardStep step : mySteps) {
            step.onWizardStarting(facade);
        }

        boolean atLeastOneVisibleStep = false;
        for (ModelWizardStep step : mySteps) {
            if (shouldShowStep(step)) {
                atLeastOneVisibleStep = true;
                break;
            }
        }

        if (atLeastOneVisibleStep) {
            goForward(); // Proceed to first step
        } else {
            // Normally we'd leave it up to external code to dispose the wizard, but since we're throwing
            // an exception in the constructor, it means the caller won't be able to get a reference to
            // this wizard before the exception interrupts it. So we manually clean things up ourselves.
            Disposer.dispose(this);
            throw new IllegalStateException("Trying to create a wizard but no steps are visible");
        }
    }

    /**
     * Boolean property which is set to {@code true} when there's a previous step we can go back to.
     * <p/>
     * The return type is an observable boolean so a UI can bind a back button to its value.
     */
    @NotNull
    public ObservableBool canGoBack() {
        return myCanGoBack;
    }

    /**
     * Boolean property which is set to {@code true} when there's a step we can move forward to.
     * Note that this can be {@code true} even on the last page - it just indicates that the current
     * step is satisfied with the information it has.
     * <p/>
     * The return type is an observable boolean so a UI can bind a back button to its value.
     */
    @NotNull
    public ObservableBool canGoForward() {
        return myCanGoForward;
    }

    /**
     * Boolean property which is set to {@code true} when the wizard is on the last step.
     * <p/>
     * The return type is an observable boolean so a UI can bind a finish button to its value.
     */
    @NotNull
    public ObservableBool onLastStep() {
        return myOnLastStep;
    }

    /**
     * String property containing the active title of the current wizard on the current step.
     * <p/>
     * This class itself is not responsible for displaying the title; rather, this task is delegated
     * to an external UI.
     * <p/>
     * The return type is an observable string so a UI can bind a label to its value.
     */
    @NotNull
    public ObservableString title() {
        return myTitle;
    }

    /**
     * Returns the component on the current step which wants to have initial focus, if any, or
     * {@code null} otherwise.
     * <p/>
     * This is provided in case the container UI wants to know (as {@link DialogWrapper} does, for
     * example).
     */
    @Nullable
    public JComponent getPreferredFocusComponent() {
        if (isFinished()) {
            return null;
        }

        ModelWizardStep step = mySteps.get(myCurrIndex);
        return step.getPreferredFocusComponent();
    }

    /**
     * Populates the wizard with an additional step (and any dependent steps it may have).
     */
    private void addStep(@NotNull ModelWizardStep<?> step) {
        myContentPanel.add(step.getComponent(), Integer.toString(mySteps.size()));
        mySteps.add(step);

        for (ModelWizardStep subStep : step.createDependentSteps()) {
            myStepOwners.put(subStep, step);
            addStep(subStep);
        }
    }

    /**
     * Returns the currently active step.
     * <p/>
     * Calling this method after a wizard has finished should not be done and has undefined behavior.
     */
    @VisibleForTesting
    @NotNull
    ModelWizardStep getCurrentStep() {
        return mySteps.get(myCurrIndex);
    }

    /**
     * Returns an (optional) action to be shown in addition to the normal wizard actions (next, previous, etc.).
     * This action is provided by the current wizard step.
     */
    @NotNull
    ObservableOptional<Action> getExtraAction() {
        return myExtraAction;
    }

    /**
     * Returns the panel that will contain the UI for each step. It is up to an external UI class
     * to decorate this with buttons (next, prev, etc.), titles, icons, etc.
     */
    @NotNull
    public JPanel getContentPanel() {
        return myContentPanel;
    }

    /**
     * Moves the wizard to the next page. If we're currently on the last page, then this action
     * finishes the wizard.
     * <p/>
     * It is an error to call this on a wizard that has already finished.
     */
    public void goForward() {
        ensureWizardIsRunning();

        if (myCurrIndex >= 0) {
            ModelWizardStep currStep = mySteps.get(myCurrIndex);
            if (!currStep.canGoForward().get()) {
                throw new IllegalStateException("Can't call goForward on wizard when the step prevents it");
            }

            myPrevSteps.add(currStep);
            currStep.onProceeding();
        }

        while (true) {
            myCurrIndex++;
            if (myCurrIndex >= mySteps.size()) {
                handleFinished(true);
                break;
            }

            ModelWizardStep step = mySteps.get(myCurrIndex);
            if (shouldShowStep(step)) {
                updateNavigationProperties();
                step.onEntering();
                showCurrentStep();
                break;
            }
        }
    }

    /**
     * Returns the wizard back to the previous page.
     * <p/>
     * It is an error to call this if there are no previous pages to return to or on a wizard that's
     * already finished.
     */
    public void goBack() {
        ensureWizardIsRunning();

        if (myPrevSteps.empty()) {
            throw new IllegalStateException("Calling back on wizard without any previous pages");
        }

        ModelWizardStep currStep = mySteps.get(myCurrIndex);
        if (!currStep.canGoBack()) {
            throw new IllegalStateException("Can't call goBack on wizard when the step prevents it");
        }

        myCurrIndex = mySteps.indexOf(myPrevSteps.pop());
        updateNavigationProperties();
        showCurrentStep();
    }

    /**
     * Cancels the wizard, discarding all work done so far.
     * <p/>
     * It is an error to call this on a wizard that has already finished.
     */
    public void cancel() {
        ensureWizardIsRunning();

        handleFinished(false);
    }

    public void addResultListener(@NotNull ResultListener listener) {
        myResultListeners.add(listener);
    }

    public void removeResultListener(@NotNull ResultListener listener) {
        myResultListeners.remove(listener);
    }

    public boolean isFinished() {
        return myCurrIndex >= mySteps.size();
    }

    private void ensureWizardIsRunning() {
        if (isFinished()) {
            throw new IllegalStateException("Invalid operation attempted after wizard already finished");
        }
    }

    private void handleFinished(boolean success) {
        if (success) {
            Set<WizardModel> seenModels = Sets.newHashSet();
            for (ModelWizardStep step : myPrevSteps) {
                WizardModel model = step.getModel();
                if (seenModels.contains(model)) {
                    continue;
                }
                seenModels.add(model);
                model.handleFinished();
            }
        }

        myCurrIndex = mySteps.size(); // Magic value indicates done. See: isFinished
        myPrevSteps.clear();
        myCanGoBack.set(false);
        myCanGoForward.set(false);
        myOnLastStep.set(false);

        // Make a copy of the event list, as a listener may attempt to remove their listener when this
        // is fired.
        List<ResultListener> listenersCopy = Lists.newArrayList(myResultListeners);
        for (ResultListener listener : listenersCopy) {
            listener.onWizardFinished(success);
        }
    }

    private void showCurrentStep() {
        ModelWizardStep step = mySteps.get(myCurrIndex);
        myTitle.set(step.getTitle());
        myExtraAction.setNullableValue(step.getExtraAction());
        ((CardLayout) myContentPanel.getLayout()).show(myContentPanel, Integer.toString(myCurrIndex));

        JComponent focusedComponent = step.getPreferredFocusComponent();
        if (focusedComponent != null) {
            IdeFocusManager.findInstanceByComponent(focusedComponent).requestFocus(focusedComponent, false);
        }
    }

    /**
     * Update the navigational properties (next, prev, etc.) given the state of the current step.
     * This should only be called if you're already on a step.
     */
    private void updateNavigationProperties() {
        myOnLastStep.set(isOnLastVisibleStep());
        ModelWizardStep step = mySteps.get(myCurrIndex);
        myBindings.bind(myCanGoForward, step.canGoForward());
        myCanGoBack.set(!myPrevSteps.empty() && step.canGoBack());
    }

    private boolean shouldShowStep(ModelWizardStep step) {
        ModelWizardStep currStep = step;
        do {
            if (!currStep.shouldShow()) {
                return false;
            }

            currStep = myStepOwners.get(currStep);
        } while (currStep != null);

        return true;
    }

    private boolean isOnLastVisibleStep() {
        float size = mySteps.size();
        boolean currPageIsLast = true;
        for (int i = myCurrIndex + 1; i < size; i++) {
            ModelWizardStep step = mySteps.get(i);
            if (shouldShowStep(step)) {
                currPageIsLast = false;
                break;
            }
        }

        return currPageIsLast;
    }

    @Override
    public void dispose() {
        myBindings.releaseAll();
        myResultListeners.clear();
    }

    /**
     * Listener interface which is fired when the wizard is either finished or canceled.
     */
    public interface ResultListener {
        void onWizardFinished(boolean success);
    }

    /**
     * In order to construct a wizard, you must do so through its builder. The builder collects steps
     * and, when the user is ready, can instantiate a new wizard which is already set to the first
     * step.
     */
    public static final class Builder {
        private final List<ModelWizardStep> mySteps;

        /**
         * Builder constructor which, for convenience, accepts some initial steps. You can also call
         * {@link #addStep(ModelWizardStep)} to add additional steps. Once all steps are added, you
         * should {@link #build()} the wizard.
         */
        public Builder(@NotNull ModelWizardStep... steps) {
            mySteps = Lists.newArrayList(steps);
        }

        public Builder addStep(@NotNull ModelWizardStep step) {
            mySteps.add(step);
            return this;
        }

        public ModelWizard build() {
            return new ModelWizard(mySteps);
        }
    }

    /**
     * Class to provide an interface providing some limited subset of wizard functionality, useful
     * to pass to steps so they can modify only the parts of the wizard that they should care about.
     */
    public final class Facade {

        /**
         * Update the properties driving next, back, and last page behavior. This is often handled
         * automatically, but a step may modify a model that will cause a later step to skip itself,
         * and there's no way the wizard can know that, so this method is provided as a way for a step
         * to manually trigger the update.
         */
        public void updateNavigationProperties() {
            if (myCurrIndex < 0) {
                return; // Protects against user calling this method in ModelWizardStep#onWizardStarting
            }
            ModelWizard.this.updateNavigationProperties();
        }

        /**
         * Allows the child step to move the wizard to the next step. If the wizard is on its last
         * step, then this action finishes the wizard.
         *
         * This should be used very sparingly, as normally you should encourage the user to navigate
         * the wizard via the UI and not do it directly. However, this can be useful if you have a
         * UI interaction where it's obvious that the user is making a clear choice and wants to move
         * forward with it, like double-clicking an item from a grid, etc.
         *
         * Because this class is passed to child steps before the wizard has even started, this method
         * will throw an exception if called too early. The step is expected to delay the call at least
         * until the wizard has started, such as on a button press or other UI event.
         *
         * @return {@code true} if the wizard moved forward, {@code false} if progress was blocked
         */
        public boolean goForward() {
            if (myCurrIndex < 0) {
                // Protects against user calling this method directly in ModelWizardStep#onWizardStarting
                throw new IllegalStateException("Attempting to goForward before the wizard has even started");
            }

            if (canGoForward().get()) {
                ModelWizard.this.goForward();
                return true;
            } else {
                return false;
            }
        }

        /**
         * Allows the child step to cancel the wizard. This should be used very sparingly, as normally
         * you should encourage the user to cancel the wizard via the UI and not do it directly.
         * However, this can be useful if you need to, say, close the wizard after some timeout passed,
         * or close it if you instead intend to finish the rest of the wizard's work on a background
         * task, etc.
         *
         * Because this class is passed to child steps before the wizard has even started, this method
         * will throw an exception if called too early. The step is expected to delay the call at least
         * until the wizard has started, such as on a button press or other UI event.
         */
        public void cancel() {
            if (myCurrIndex < 0) {
                // Protects against user calling this method directly in ModelWizardStep#onWizardStarting
                throw new IllegalStateException("Attempting to cancel before the wizard has even started");
            }
            ModelWizard.this.cancel();
        }
    }
}