| /* |
| * Copyright (C) 2014 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.dynamic; |
| |
| import com.android.tools.idea.wizard.template.TemplateWizard; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.intellij.ide.wizard.Step; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.UndoConfirmationPolicy; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.wm.IdeFocusManager; |
| import com.intellij.util.ui.update.MergingUpdateQueue; |
| import com.intellij.util.ui.update.Update; |
| import icons.AndroidIcons; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Key; |
| |
| /** |
| * DynamicWizard is an evolution of {@link TemplateWizard} that seeks to provide a flexible base for |
| * implemented GUI wizards that may involve multiple steps and branches. |
| * |
| * A DynamicWizard contains a series of {@link DynamicWizardPath}s, in the order that the user is expected |
| * to traverse through the wizard. Paths may declare themselves to be visible or invisible (in which case they |
| * will be skipped by the wizard), depending on the |
| * state of the wizard. Each path contains a series of {@link DynamicWizardStep}s, which also may be visible or invisible, depending |
| * on the state of the wizard. The DynamicWizard class is responsible for providing the GUI frame for the wizard and |
| * maintaining the state of the wizard buttons. Each path and step is responsible for its own validation and visibility. Each step must |
| * provide a {@link JComponent} that serves as the user interface for that step. Each step also provides a title string and optionally |
| * provides an {@link Icon} to be displayed on the left hand side of the wizard pane. |
| * |
| * |
| */ |
| public abstract class DynamicWizard implements ScopedStateStore.ScopedStoreListener { |
| // 42 is an arbitrary number. This constant is for the number of update cycles before |
| // we decide there's circular dependency and we cannot settle down the model state. |
| public static final int MAX_UPDATE_ATTEMPTS = 42; |
| // TODO: Make this logger private and create new loggers for anywhere that complains |
| public static Logger LOG = Logger.getInstance(DynamicWizard.class); |
| |
| // A queue of updates used to throttle the update() function. |
| private final MergingUpdateQueue myUpdateQueue; |
| // Used by update() to ensure that multiple updates are not invoked simultaneously. |
| private boolean myUpdateInProgress; |
| // A reference to the project context in which this wizard was invoked. |
| @Nullable private Project myProject; |
| // A reference to the module context in which this wizard was invoked. |
| @Nullable private Module myModule; |
| |
| // Wizard "chrome" |
| @NotNull protected final DynamicWizardHost myHost; |
| // The name of this wizard for display to the user |
| protected String myName; |
| // List of the paths that this wizard contains. Paths can be optional or required. |
| protected ArrayList<AndroidStudioWizardPath> myPaths = Lists.newArrayList(); |
| // The current path |
| protected AndroidStudioWizardPath myCurrentPath; |
| // An iterator to keep track of the user's progress through the paths. |
| protected PathIterator myPathListIterator = new PathIterator(myPaths); |
| private boolean myIsInitialized = false; |
| protected ScopedStateStore myState; |
| private JPanel myContentPanel = new JPanel(new CardLayout()); |
| private Map<JComponent, String> myComponentToIdMap = Maps.newHashMap(); |
| |
| public DynamicWizard(@Nullable Project project, @Nullable Module module, @NotNull String name) { |
| this(project, module, name, new DialogWrapperHost(project)); |
| } |
| |
| public DynamicWizard(@Nullable Project project, @Nullable Module module, @NotNull String name, @NotNull DynamicWizardHost host) { |
| myHost = host; |
| myProject = project; |
| myModule = module; |
| myName = name; |
| myHost.setTitle(name); |
| Application application = ApplicationManager.getApplication(); |
| if (application == null || application.isUnitTestMode()) { |
| myUpdateQueue = null; |
| } else { |
| myUpdateQueue = new MergingUpdateQueue("wizard", 100, true, MergingUpdateQueue.ANY_COMPONENT, myHost.getDisposable(), null, true); |
| } |
| myState = new ScopedStateStore(ScopedStateStore.Scope.WIZARD, null, this); |
| } |
| |
| public void init() { |
| myHost.init(this); |
| myIsInitialized = true; |
| |
| if (myUpdateQueue != null) { |
| int guard = 0; |
| // Keep processing updates until model state settles down. |
| // In some cases, circular dependencies may turn this into endless loop. This is coding |
| // error so we need to detect it and report to developer. |
| while (!myUpdateQueue.isEmpty()) { |
| myUpdateQueue.flush(); |
| guard++; |
| if (guard >= MAX_UPDATE_ATTEMPTS) { |
| throw new IllegalStateException("Circular dependencies detected. Model state cannot be settled down."); |
| } |
| } |
| } |
| Step step = showNextStep(null); |
| assert step != null; |
| } |
| |
| /** |
| * @return update queue that other components may use to submit their updates. |
| */ |
| @Nullable |
| public final MergingUpdateQueue getUpdateQueue() { |
| return myUpdateQueue; |
| } |
| |
| /** |
| * Call update with rate throttling |
| */ |
| @Override |
| public <T> void invokeUpdate(@Nullable Key<T> changedKey) { |
| if (myUpdateQueue != null) { |
| myUpdateQueue.queue(new WizardUpdate()); |
| } |
| else { |
| // If we're not running in a context, just update immediately |
| update(); |
| } |
| } |
| |
| /** |
| * Updating: Whenever a path's update method is called with a WIZARD scope, |
| * it will invoke the parent Wizard's update method. This update method is rate throttled. |
| */ |
| |
| /** |
| * Call the update steps in order. Will not fire if an update is already in progress. |
| */ |
| private void update() { |
| if (!myUpdateInProgress) { |
| myUpdateInProgress = true; |
| deriveValues(myState.getRecentUpdates()); |
| myUpdateInProgress = false; |
| } |
| } |
| |
| /** |
| * Takes the list of changed variables and uses them to recalculate any variables |
| * which may depend on those changed values. |
| * @param modified map of the keys of the changed objects in the state store to their scopes. |
| */ |
| public void deriveValues(Set<Key> modified) { |
| |
| } |
| |
| /** |
| * Declare any finishing actions that will take place at the completion of the wizard. This will |
| * be executed by a worker thread, under progress. |
| */ |
| public abstract void performFinishingActions(); |
| |
| /** |
| * Get the project context which this wizard is operating under. |
| * If the this wizard is a global one, the function returns null. |
| */ |
| @Nullable |
| public Project getProject() { |
| return myProject; |
| } |
| |
| /** |
| * Get the module context which this wizard is operating under. |
| * If the this wizard is a global one or project-scoped, the function returns null. |
| */ |
| @Nullable |
| protected final Module getModule() { |
| return myModule; |
| } |
| |
| /** |
| * Converts the given text to an HTML message if necessary, and then displays it to the user. |
| * @param errorMessage the message to display |
| */ |
| public final void setErrorHtml(String errorMessage) { |
| if (myCurrentPath != null) { |
| myCurrentPath.setErrorHtml(errorMessage); |
| } |
| } |
| |
| /** |
| * Update the buttons for the wizard |
| * @param canGoPrev whether the previous button is enabled |
| * @param canGoNext whether the next button is enabled |
| * @param canCancelCurrentPath whether the cancel button is enabled |
| * @param canFinishCurrentPath if this is set to true and the current path is the last non-optional path, the canFinish |
| */ |
| public final void updateButtons(boolean canGoPrev, boolean canGoNext, boolean canCancelCurrentPath, boolean canFinishCurrentPath) { |
| if (!myIsInitialized) { |
| // Buttons were not yet created |
| return; |
| } |
| myHost.updateButtons(canGoPrev && hasPrevious(), canGoNext && hasNext(), |
| canCancelCurrentPath && canCancel(), canFinishCurrentPath && canFinish()); |
| } |
| |
| /** |
| * Add the given path to the end of this wizard. |
| */ |
| public final void addPath(@NotNull AndroidStudioWizardPath path) { |
| myPaths.add(path); |
| path.attachToWizard(this); |
| // Rebuild the iterator to avoid concurrent modification exceptions |
| myPathListIterator = new PathIterator(myPaths); |
| } |
| |
| @NotNull |
| public final ArrayList<AndroidStudioWizardPath> getAllPaths() { |
| return myPaths; |
| } |
| |
| @Nullable |
| public final AndroidStudioWizardPath getCurrentPath() { |
| return myCurrentPath; |
| } |
| |
| /** |
| * @return the total number of visible steps in this wizard. |
| */ |
| public final int getVisibleStepCount() { |
| int sum = 0; |
| for (AndroidStudioWizardPath path : myPaths) { |
| sum += path.getVisibleStepCount(); |
| } |
| return sum; |
| } |
| |
| private void showStep(@NotNull Step step) { |
| JComponent component = step.getComponent(); |
| addStepIfNecessary(step); |
| Icon icon = step.getIcon(); |
| myHost.setIcon(icon); |
| ((CardLayout)myContentPanel.getLayout()).show(myContentPanel, myComponentToIdMap.get(component)); |
| |
| JComponent focusedComponent = step.getPreferredFocusedComponent(); |
| if (focusedComponent != null) { |
| IdeFocusManager.findInstanceByComponent(focusedComponent).requestFocus(focusedComponent, false); |
| } |
| } |
| |
| /** |
| * @return true if the wizard can advance to the next step. Returns false if there is an error |
| * on the current step or if there are no more steps. Subclasses should rarely need to override |
| * this method. |
| */ |
| protected boolean canGoNext() { |
| return myCurrentPath != null && myCurrentPath.canGoNext(); |
| } |
| |
| /** |
| * @return true if the wizard can go back to the previous step. Returns false if there is an error |
| * on the current step or if there are no more steps prior to the current one. |
| * Subclasses should rarely need to override this method. |
| */ |
| protected boolean canGoPrevious() { |
| return myCurrentPath != null && myCurrentPath.canGoPrevious(); |
| } |
| |
| /** |
| * @return true if the wizard has additional visible steps. Subclasses should rarely need to override |
| * this method. |
| */ |
| protected boolean hasNext() { |
| return myCurrentPath != null && myCurrentPath.hasNext() || myPathListIterator.hasNext(); |
| } |
| |
| /** |
| * @return true if the wizard has previous visible steps |
| * Subclasses should rarely need to override this method. |
| */ |
| protected boolean hasPrevious() { |
| return myCurrentPath != null && myCurrentPath.hasPrevious() || myPathListIterator.hasPrevious(); |
| } |
| |
| /** |
| * @return true if the wizard is in a state in which it can finish. This is defined as being done with the current |
| * path and having no required paths remaining. Subclasses should rarely need to override |
| * this method. |
| */ |
| protected boolean canFinish() { |
| if (!myPathListIterator.hasNext() && (myCurrentPath == null || !myCurrentPath.hasNext())) { |
| return true; |
| } else if (myCurrentPath != null && myCurrentPath.hasNext()) { |
| return false; |
| } |
| |
| boolean canFinish = true; |
| PathIterator remainingPaths = myPathListIterator.getFreshCopy(); |
| while(canFinish && remainingPaths.hasNext()) { |
| canFinish = !remainingPaths.next().isPathRequired(); |
| } |
| return canFinish; |
| } |
| |
| /** |
| * @return true iff the current step is the last one in the wizard (required or optional) |
| */ |
| protected final boolean isLastStep() { |
| if (myCurrentPath != null) { |
| return !myPathListIterator.hasNext() && !myCurrentPath.hasNext(); |
| } else { |
| return !myPathListIterator.hasNext(); |
| } |
| } |
| |
| /** |
| * Commit the current step and move to the next step. Subclasses should rarely need to override |
| * this method. |
| */ |
| public final void doNextAction() { |
| if (!canAdvance()) { |
| myHost.shakeWindow(); |
| return; |
| } |
| Step newStep = showNextStep(myCurrentPath); |
| if (newStep == null) { |
| doFinishAction(); |
| } |
| } |
| |
| @Nullable |
| private Step showNextStep(@Nullable AndroidStudioWizardPath path) { |
| Step newStep; |
| if (path != null && path.hasNext()) { |
| newStep = path.next(); |
| } |
| else { |
| newStep = null; |
| while (myPathListIterator.hasNext() && newStep == null) { |
| myCurrentPath = myPathListIterator.next(); |
| assert myCurrentPath != null; |
| myCurrentPath.onPathStarted(true /* fromBeginning */); |
| newStep = myCurrentPath.getCurrentStep(); |
| } |
| } |
| if (newStep != null) { |
| showStep(newStep); |
| } |
| return newStep; |
| } |
| |
| /** |
| * Test if current step and/or path are ok with moving to a next step or completing the wizard. |
| */ |
| private boolean canAdvance() { |
| if (myCurrentPath == null) { |
| return true; |
| } |
| else if (myCurrentPath.canGoNext()) { |
| return myCurrentPath.hasNext() || myCurrentPath.readyToLeavePath(); |
| } |
| else { |
| return false; |
| } |
| } |
| |
| /** |
| * Find and go to the previous step. Subclasses should rarely need to override |
| * this method. |
| */ |
| public final void doPreviousAction() { |
| assert myCurrentPath != null; |
| if (!myCurrentPath.canGoPrevious()) { |
| myHost.shakeWindow(); |
| return; |
| } |
| |
| Step newStep; |
| if (myCurrentPath == null || !myCurrentPath.hasPrevious()) { |
| newStep = null; |
| while (myPathListIterator.hasPrevious() && newStep == null) { |
| myCurrentPath = myPathListIterator.previous(); |
| assert myCurrentPath != null; |
| myCurrentPath.onPathStarted(false /* fromBeginning */); |
| newStep = myCurrentPath.getCurrentStep(); |
| } |
| } |
| else if (myCurrentPath.hasPrevious()) { |
| newStep = myCurrentPath.previous(); |
| } |
| else { |
| myHost.close(DynamicWizardHost.CloseAction.CANCEL); |
| return; |
| } |
| if (newStep != null) { |
| showStep(newStep); |
| } |
| else { |
| LOG.error("Stepped into Path " + myCurrentPath + " which returned a null step"); |
| } |
| } |
| |
| /** |
| * Complete the wizard, doing any finishing actions that have been queued up during the wizard flow, |
| * with a progress indicator. Subclasses should rarely need to override this method. |
| */ |
| public void doFinishAction() { |
| if (myCurrentPath != null && !myCurrentPath.readyToLeavePath()) { |
| myHost.shakeWindow(); |
| return; |
| } |
| myHost.close(DynamicWizardHost.CloseAction.FINISH); |
| |
| ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); |
| doFinish(); |
| } |
| catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| }, getProgressTitle(), false, getProject(), getProgressParentComponent()); |
| } |
| |
| /** |
| * The component that should be the parent of the progress window created on wizard |
| * completion. Null by default: the main window will be used. |
| * Subclasses should override this if the wizard is kicked off from a window other than the main |
| * Studio window; otherwise the progress bar will be beneath that window. |
| */ |
| @Nullable |
| public JComponent getProgressParentComponent() { |
| return null; |
| } |
| |
| @NotNull |
| protected abstract String getProgressTitle(); |
| |
| /** |
| * Cancel the wizard |
| */ |
| public void doCancelAction() { |
| myHost.close(DynamicWizardHost.CloseAction.CANCEL); |
| } |
| |
| protected UndoConfirmationPolicy getUndoConfirmationPolicy() { |
| return UndoConfirmationPolicy.DEFAULT; |
| } |
| |
| @Nullable |
| public final JComponent getPreferredFocusedComponent() { |
| Step currentStep = myCurrentPath.getCurrentStep(); |
| if (currentStep != null) { |
| return currentStep.getPreferredFocusedComponent(); |
| } |
| else { |
| return null; |
| } |
| } |
| |
| protected abstract String getWizardActionDescription(); |
| |
| /** |
| * @return the scoped state store associate with this wizard as a whole |
| */ |
| public final ScopedStateStore getState() { |
| return myState; |
| } |
| |
| private void prepareForShow() { |
| // All steps must be included so the window can be sized correctly |
| for (AndroidStudioWizardPath path : myPaths) { |
| for (Step step : path.getAllSteps()) { |
| addStepIfNecessary(step); |
| } |
| } |
| |
| SwingUtilities.getWindowAncestor(myContentPanel).pack(); |
| } |
| |
| private void addStepIfNecessary(Step step) { |
| JComponent component = step.getComponent(); |
| String id = myComponentToIdMap.get(component); |
| if (id == null) { |
| id = String.valueOf(myComponentToIdMap.size()); |
| myComponentToIdMap.put(component, id); |
| myContentPanel.add(component, id); |
| } |
| } |
| |
| public final void show() { |
| prepareForShow(); |
| myHost.show(); |
| } |
| |
| @NotNull |
| public Disposable getDisposable() { |
| return myHost.getDisposable(); |
| } |
| |
| public boolean showAndGet() { |
| prepareForShow(); |
| return myHost.showAndGet(); |
| } |
| |
| public final Component getContentPane() { |
| return myContentPanel; |
| } |
| |
| @Nullable |
| public String getHelpId() { |
| return null; |
| } |
| |
| public void setTitle(String title) { |
| myHost.setTitle(title); |
| } |
| |
| /** |
| * Returns true if a step with the given name exists in this wizard's current configuration. |
| * If visibleOnly is set to true, only visible steps (that are part of visible paths) will |
| * be considered. |
| */ |
| public boolean containsStep(@NotNull String stepName, boolean visibleOnly) { |
| for (AndroidStudioWizardPath path : myPaths) { |
| if (visibleOnly && !path.isPathVisible()) { |
| continue; |
| } |
| if (path.containsStep(stepName, visibleOnly)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Navigates this wizard to the step with the given name if it exists. If not, this function |
| * is a no-op. If the requireVisible parameter is set to true, then only currently visible steps (which |
| * are part of currently visible paths) will be considered. |
| */ |
| public void navigateToNamedStep(@NotNull String stepName, boolean requireVisible) { |
| for (AndroidStudioWizardPath path : myPaths) { |
| if ((!requireVisible || path.isPathVisible()) && path.containsStep(stepName, requireVisible)) { |
| myCurrentPath = path; |
| myPathListIterator.myCurrentIndex = myPathListIterator.myList.indexOf(myCurrentPath); |
| myCurrentPath.navigateToNamedStep(stepName, requireVisible); |
| showStep(myCurrentPath.getCurrentStep()); |
| return; |
| } |
| } |
| } |
| |
| public boolean canCancel() { |
| return true; |
| } |
| |
| @Nullable |
| public Icon getIcon() { |
| return AndroidIcons.Wizards.NewProjectMascotGreen; |
| } |
| |
| protected static class PathIterator { |
| |
| private int myCurrentIndex; |
| private ArrayList<AndroidStudioWizardPath> myList; |
| |
| public PathIterator(ArrayList<AndroidStudioWizardPath> list) { |
| myList = list; |
| myCurrentIndex = -1; |
| } |
| |
| /** |
| * @return a copy of this iterator |
| */ |
| public PathIterator getFreshCopy() { |
| PathIterator toReturn = new PathIterator(myList); |
| toReturn.myCurrentIndex = myCurrentIndex; |
| return toReturn; |
| } |
| |
| /** |
| * @return true iff there are more visible paths with steps following the current location |
| */ |
| public boolean hasNext() { |
| if (myCurrentIndex >= myList.size() - 1) { |
| return false; |
| } |
| for (int i = myCurrentIndex + 1; i < myList.size(); i++) { |
| AndroidStudioWizardPath path = myList.get(i); |
| if (path.isPathVisible() && path.getVisibleStepCount() > 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @return true iff this path has more visible steps previous to its current step |
| */ |
| public boolean hasPrevious() { |
| if (myCurrentIndex <= 0) { |
| return false; |
| } |
| for (int i = myCurrentIndex - 1; i >= 0; i--) { |
| if (myList.get(i).isPathVisible()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Advance to the next visible path and return it, or null if there are no following visible paths |
| * @return the next path |
| */ |
| @Nullable |
| public AndroidStudioWizardPath next() { |
| while (myCurrentIndex < (myList.size() - 1)) { |
| AndroidStudioWizardPath next = myList.get(++myCurrentIndex); |
| if (next.isPathVisible()) { |
| return next; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Go back to the last visible path and return it, or null if there are no previous visible paths |
| */ |
| @Nullable |
| public AndroidStudioWizardPath previous() { |
| do { |
| myCurrentIndex--; |
| } while(myCurrentIndex >= 0 && !myList.get(myCurrentIndex).isPathVisible()); |
| if (myCurrentIndex >= 0) { |
| return myList.get(myCurrentIndex); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| protected void doFinish() throws IOException { |
| for (AndroidStudioWizardPath path : myPaths) { |
| if (path.isPathVisible()) { |
| path.performFinishingActions(); |
| } |
| } |
| performFinishingActions(); |
| } |
| |
| private class WizardUpdate extends Update { |
| public WizardUpdate() { |
| super("Wizard Update"); |
| } |
| |
| @NotNull |
| @Override |
| public Object[] getEqualityObjects() { |
| return new Object[]{DynamicWizard.this}; |
| } |
| |
| @Override |
| public void run() { |
| update(); |
| } |
| } |
| } |