blob: b31d82ca4f0bbfd12bd76133ae95df7cb8e51ff5 [file] [log] [blame]
/*
* 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.WizardConstants;
import com.intellij.ide.wizard.CommitStepException;
import com.intellij.ide.wizard.Step;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.Map;
import java.util.Set;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Key;
import static com.android.tools.idea.wizard.dynamic.ScopedStateStore.Scope.STEP;
/**
* DynamicWizardStep
* A DynamicWizardStep is the smallest unit of a workflow.
* It is responsible for creating UI that will live inside the frame provided by the wizard and linking that UI to a {@link ScopedStateStore}.
* A subclass of DynamicWizardStep must implement functions that return the name of the step,
* the JComponent associated with the UI of the step, and a label where error messages can be displayed to the user.
* Additionally, each step can override {@link #validate()} to implement custom data validation whenever the user enters new data,
* and {@link #isStepVisible()} to affect whether the step is shown to the user.
*
* It is worth noting that there’s a utility subclass of DynamicWizardStep called
* {@link DynamicWizardStepWithHeaderAndDescription}
* which has some UI enhancements such as a title and a description bar that will update when a component is focused.
*/
public abstract class DynamicWizardStep extends ScopedDataBinder implements Step {
private static final Logger LOG = Logger.getInstance(DynamicWizardStep.class);
// Reference to the parent path.
protected DynamicWizardPath myPath;
// Panel that contains wizard step controls
private JPanel myRootPane;
// Used by update() to ensure multiple update steps are not run at the same time
private boolean myUpdateInProgress;
// used by update() to save whether this step is valid and the wizard can progress.
private boolean myIsValid;
private boolean myInitialized;
protected WizardStepHeaderPanel myHeader;
// Used to postpone and coalesce update requests
@Nullable MergingUpdateQueue myUpdateQueue;
public DynamicWizardStep() {
myState = new ScopedStateStore(STEP, null, this);
}
/**
* Attach this step to the given path, linking the state store.
*/
public final void attachToPath(@NotNull DynamicWizardPath path) {
myPath = path;
Map<String, Object> myCurrentValues = myState.flatten();
myState = new ScopedStateStore(STEP, myPath.getState(), this);
for (String keyName : myCurrentValues.keySet()) {
myState.put(myState.createKey(keyName, Object.class), myCurrentValues.get(keyName));
}
myUpdateQueue = path.getUpdateQueue();
}
/**
* Set up this step. UI initialization should be done here.
* This initialization will only be performed once, during the first time
* this step appears on screen. Any work that should be done every time the
* user interacts with a step should be done in {@link #onEnterStep()}.
*/
public abstract void init();
/**
* Get the project context which this wizard is operating under.
* If the this wizard is a global one, this function returns null.
*/
@Nullable
protected final Project getProject() {
return myPath != null ? myPath.getProject() : null;
}
/**
* 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 myPath != null ? myPath.getModule() : null;
}
@Nullable
protected final DynamicWizard getWizard() {
if (myPath != null) {
return myPath.getWizard();
}
return null;
}
/**
* Optionally add an icon to the left side of the screen.
* @return An icon to be displayed on the left side of the wizard.
*/
@Override
@Nullable
public Icon getIcon() {
return null;
}
/**
* Legacy compatibility
*/
@Deprecated
@Override
public final void _init() {
onEnterStep();
}
/**
* Legacy compatibility
*/
@Deprecated
@Override
public final void _commit(boolean finishChosen) throws CommitStepException {
commitStep();
}
/**
* Called when the step is entered.
* The step should do any initialization/update work here to prepare for user interaction.
*/
public void onEnterStep() {
if (!myInitialized) {
init();
myInitialized = true;
}
invokeUpdate(null);
}
/**
* Called when the user tries to advance to the next step.
* Any data commitment or actions taken by the step should be declared in this function.
* @return true if the step was committed successfully and the wizard can progress. false if the
* wizard should remain on this step.
*/
public boolean commitStep() {
return true;
}
/**
* Determine whether this step is visible to the user. Visibility is updated automatically whenever
* a parameter changes.
* @return true if this step should be visible to the user. false otherwise.
*/
public boolean isStepVisible() {
return true;
}
/**
* Updating: Whenever the user updates the value of a UI element, the update() function is called.
* Each update cycle consists of three steps:
* 1) updateModel (updates the model state to match the UI state)
* 2) deriveValues (updates values that depend on other values)
* 3) validate (checks the given input for correctness and consistency)
*/
/**
* update this step. Will invoke the parent path's update function if the
* scope is PATH or WIZARD. Should generally not be overridden.
*/
@Override
public <T> void invokeUpdate(@Nullable final Key<T> changedKey) {
if (myUpdateQueue != null) {
myUpdateQueue.queue(new StepUpdate(changedKey));
}
else {
performUpdate(changedKey);
}
}
private <T> void performUpdate(@Nullable Key<T> changedKey) {
if (!myInitialized) {
return;
}
super.invokeUpdate(changedKey);
update();
if (myPath != null) {
myPath.updateButtons();
}
}
/**
* Call the three update steps in order. Will not fire if an update is already in progress.
*/
private void update() {
if (!myUpdateInProgress) {
myUpdateInProgress = true;
updateModelFromUI();
deriveValues(myState.getRecentUpdates());
myIsValid = validate();
myState.clearRecentUpdates();
myUpdateInProgress = false;
}
}
/**
* If a UI element is registered against a key/scope pair, the listener for that UI element will
* automatically update the model state every time the value is changed. If additional work is
* necessary for pulling values from the UI and inserting them into the model it may be done here.
* Most subclasses should not have a reason to override this part of the cycle.
*/
public void updateModelFromUI() {
}
/**
* The second step in the update cycle. Takes the list of changed variables and uses them to recalculate any variables
* which may depend on those changed values. Alternatively, a {@link ScopedDataBinder.ValueDeriver} may be registered
* which will be called to update the value associated with a single key.
* @param modified set of the changed keys
*/
public void deriveValues(Set<Key> modified) {
}
/**
* Third step in the update cycle.
* Validate the current input and return true if the current step is in a good state and the wizard can continue.
* This function is automatically called whenever the user changes the value of a UI element.
* Most subclasses will want to override this function and add custom validation.
* @return true if the current input is complete and consistent and the wizard can continue.
*/
public boolean validate() {
return true;
}
/**
* Called indirectly on every update by the wizard's updateButtons method.
* Subclasses should rarely need to override this method. It is preferred
* that subclasses override validate() and rely on the update method to set the value
* used by this determination.
* @return true if the user can progress to the next step in this path.
*/
public boolean canGoNext() {
return myIsValid;
}
/**
* Called indirectly on every update by the wizard's updateButtons method.
* Subclasses should rarely need to override this method.
* @return true if the user should be allowed to go back through this path.
*/
public boolean canGoPrevious() {
return true;
}
/**
* 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(@Nullable String errorMessage) {
JLabel label = getMessageLabel();
if (label != null) {
errorMessage = toHtml(errorMessage);
label.setText(errorMessage);
}
else {
LOG.debug("Message was displayed on a step without error label", new Exception());
}
}
/**
* Get the name of the step to be used with {@link DynamicWizard#navigateToNamedStep(String, boolean)}
* The value returned by this function will not be shown to the user by the wizard framework.
*/
@Override
public String toString() {
return getStepName();
}
/**
* Retrieve the UI for this step. UI initialization and construction should NOT be
* done in this function. This function should only return an already created and
* initialized component.
* @return A container which contains all user interface elements for this step.
*/
@Override
@NotNull
public final JComponent getComponent() {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myRootPane == null) {
myRootPane = new JPanel(new BorderLayout());
myHeader = WizardStepHeaderPanel.create(getHeaderColor(), getWizardIcon(), getStepIcon(), getStepTitle(), getStepDescription());
myRootPane.add(myHeader, BorderLayout.NORTH);
myRootPane.add(createStepBody(), BorderLayout.CENTER);
}
return myRootPane;
}
/**
* @return header color for this step
*/
@NotNull
protected JBColor getHeaderColor() {
return WizardConstants.ANDROID_NPW_HEADER_COLOR;
}
/**
* @return optional "description" icon placed on the header to the right.
*/
@Nullable
protected Icon getStepIcon() {
return null;
}
/**
* @return wizard step body
*/
@NotNull
protected abstract Component createStepBody();
@Nullable
protected Icon getWizardIcon() {
DynamicWizard wizard = getWizard();
return wizard == null ? null : wizard.getIcon();
}
/**
* Returns a label that can be used to display messages to users.
* @return a JLabel (or descendent) used to display errors and information to the user
* or <code>null</code> if no errors can be displayed on this page.
*/
@Nullable
public abstract JLabel getMessageLabel();
/**
* @return the name of the current step to be displayed to the user
*/
@NotNull
public abstract String getStepName();
@NotNull
protected abstract String getStepTitle();
@Nullable
protected abstract String getStepDescription();
/**
* Wrap the target string with html tags unless it is already tagged. If the input string is
* {@code null} then the output string will also be {@code null}.
*/
@Nullable
protected final String toHtml(@Nullable String text) {
if (!StringUtil.isEmpty(text) && !text.startsWith("<html>")) {
text = String.format("<html>%1$s</html>", text.trim());
}
return text;
}
private class StepUpdate extends Update {
private final Key<?> myChangedKey;
public StepUpdate(@Nullable Key<?> changedKey) {
super(DynamicWizardStep.this);
myChangedKey = changedKey;
}
@NotNull
@Override
public Object[] getEqualityObjects() {
return new Object[] {DynamicWizardStep.this, myChangedKey};
}
@Override
public void run() {
performUpdate(myChangedKey);
}
}
}