/*
* 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;
import com.intellij.ide.wizard.CommitStepException;
import com.intellij.ide.wizard.Step;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.ui.JBColor;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.Map;
import java.util.Set;
import static com.android.tools.idea.wizard.ScopedStateStore.Key;
import static com.android.tools.idea.wizard.ScopedStateStore.Scope.STEP;
/**
* A step in a wizard path.
*/
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;
// 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;
public DynamicWizardStep() {
myState = new ScopedStateStore(STEP, null, this);
}
public static JPanel createWizardStepHeader(JBColor headerColor, Icon icon, String title) {
JPanel panel = new JPanel();
panel.setBackground(headerColor);
panel.setBorder(new EmptyBorder(WizardConstants.STUDIO_WIZARD_INSETS));
panel.setLayout(new GridLayoutManager(2, 2, new Insets(18, 0, 12, 0), 2, 2));
GridConstraints c = new GridConstraints(0, 0, 2, 1, GridConstraints.ANCHOR_NORTHWEST,
GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(60, 60), null);
ImageComponent image = new ImageComponent(icon);
panel.add(image, c);
c = new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_SOUTHWEST, GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW | GridConstraints.SIZEPOLICY_WANT_GROW,
GridConstraints.SIZEPOLICY_FIXED, null, null, null);
JLabel titleLabel = new JLabel(title);
titleLabel.setForeground(Color.WHITE);
titleLabel.setFont(titleLabel.getFont().deriveFont(24f));
panel.add(titleLabel, c);
c.setRow(1);
c.setAnchor(GridConstraints.ANCHOR_NORTHWEST);
JLabel productLabel = new JLabel("Android Studio");
productLabel.setForeground(Color.WHITE);
panel.add(productLabel, c);
return panel;
}
/**
* 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));
}
}
/**
* Set up this step. UI initialization should be done here.
*/
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 Key<T> changedKey) {
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) {
if (errorMessage != null && !errorMessage.startsWith("<html>")) {
errorMessage = "<html>" + errorMessage + "</html>";
}
JLabel label = getMessageLabel();
if (label != null) {
label.setText(errorMessage);
}
else {
LOG.debug("Message was displayed on a step without error label", new Exception());
}
}
@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 abstract JComponent getComponent();
/**
* 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();
}