/* * Wizard.java * * Copyright (C) 2009-12 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.core.client.widget; import java.util.ArrayList; import org.rstudio.core.client.CommandWithArg; import org.rstudio.core.client.Debug; import org.rstudio.core.client.resources.ImageResource2x; import org.rstudio.core.client.theme.res.ThemeResources; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.layout.client.Layout.AnimationCallback; import com.google.gwt.layout.client.Layout.Layer; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.LayoutPanel; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; public class Wizard<I,T> extends ModalDialog<T> { public Wizard(String caption, String okCaption, I initialData, WizardPage<I, T> firstPage, final ProgressOperationWithInput<T> operation) { super(caption, operation); initialData_ = initialData; okCaption_ = okCaption; firstPage_ = firstPage; activePage_ = firstPage; resetOkButtonCaption(); setOkButtonVisible(false); addCloseHandler(new CloseHandler<PopupPanel>() { @Override public void onClose(CloseEvent<PopupPanel> arg0) { cleanupPage(firstPage_); } }); // add next button nextButton_ = new ThemedButton("Next", new ClickHandler() { @Override public void onClick(ClickEvent arg0) { if (activePage_ instanceof WizardIntermediatePage<?,?>) { final WizardIntermediatePage<I, T> page = (WizardIntermediatePage<I, T>) activePage_; // collect input from this page asynchronously and advance when // we have input page.collectIntermediateInput(getProgressIndicator(), new OperationWithInput<T>() { @Override public void execute(final T input) { // prevent re-entrancy if (validating_) return; validating_ = true; try { page.validateAsync(input, new OperationWithInput<Boolean>() { @Override public void execute(Boolean valid) { validating_ = false; if (valid) { intermediateResult_ = input; page.advance(); } } }); } catch (Exception e) { validating_ = false; Debug.logException(e); } } }); } } }); nextButton_.setVisible(false); addActionButton(nextButton_); } @Override protected void onUnload() { activePage_.onDeactivate(new Operation() { public void execute() { } }); super.onUnload(); } @Override protected Widget createMainWidget() { WizardResources res = WizardResources.INSTANCE; WizardResources.Styles styles = res.styles(); VerticalPanel mainWidget = new VerticalPanel(); mainWidget.addStyleName(getMainWidgetStyle()); headerPanel_ = new LayoutPanel(); headerPanel_.addStyleName(styles.headerPanel()); // layout constants final int kTopMargin = 5; final int kLeftMargin = 8; final int kCaptionWidth = 400; final int kCaptionHeight = 30; final int kPageUILeftMargin = 123; // first page caption subCaptionLabel_ = new Label(firstPage_.getPageCaption()); subCaptionLabel_.addStyleName(styles.headerLabel()); headerPanel_.add(subCaptionLabel_); headerPanel_.setWidgetLeftWidth(subCaptionLabel_, kTopMargin, Unit.PX, kCaptionWidth, Unit.PX); headerPanel_.setWidgetTopHeight(subCaptionLabel_, kLeftMargin, Unit.PX, kCaptionHeight, Unit.PX); // second page back button ImageResource2x bkImg = new ImageResource2x(res.wizardBackButton2x()); backButton_ = new Label("Back"); backButton_.addStyleName(styles.wizardBackButton()); backButton_.addStyleName(ThemeResources.INSTANCE.themeStyles().handCursor()); headerPanel_.add(backButton_); headerPanel_.setWidgetLeftWidth(backButton_, kTopMargin - 2, Unit.PX, bkImg.getWidth(), Unit.PX); headerPanel_.setWidgetTopHeight(backButton_, kTopMargin - 2, Unit.PX, bkImg.getHeight(), Unit.PX); backButton_.setVisible(false); backButton_.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { goBack(); } }); // second page caption label pageCaptionLabel_ = new Label(); pageCaptionLabel_.addStyleName(styles.headerLabel()); headerPanel_.add(pageCaptionLabel_); headerPanel_.setWidgetLeftWidth(pageCaptionLabel_, kPageUILeftMargin, Unit.PX, kCaptionWidth, Unit.PX); headerPanel_.setWidgetTopHeight(pageCaptionLabel_, kLeftMargin, Unit.PX, kCaptionHeight, Unit.PX); pageCaptionLabel_.setVisible(false); mainWidget.add(headerPanel_); // main body panel for transitions bodyPanel_ = new LayoutPanel(); ArrayList<String> wizardBodyStyles = getWizardBodyStyles(); for (String styleName: wizardBodyStyles) bodyPanel_.addStyleName(styleName); bodyPanel_.getElement().getStyle().setProperty("overflowX", "hidden"); mainWidget.add(bodyPanel_); // add first page (and all sub-pages recursively) addAndInitializePage(firstPage_, true); setNextButtonState(firstPage_); return mainWidget; } private void addAndInitializePage(WizardPage<I,T> page, boolean visible) { page.setSize("100%", "100%"); bodyPanel_.add(page); bodyPanel_.setWidgetTopBottom(page, 0, Unit.PX, 0, Unit.PX); bodyPanel_.setWidgetLeftRight(page, 0, Unit.PX, 0, Unit.PX); bodyPanel_.setWidgetVisible(page, visible); page.initialize(initialData_); CommandWithArg<WizardPage<I,T>> showPageCmd = new CommandWithArg<WizardPage<I,T>>() { @Override public void execute(WizardPage<I, T> page) { showPage(page); }; }; if (page instanceof WizardNavigationPage<?,?>) { ((WizardNavigationPage<I,T>) page).setSelectionHandler(showPageCmd); } else if (page instanceof WizardIntermediatePage<?,?>) { ((WizardIntermediatePage<I,T>) page).setNextHandler(showPageCmd); } // recursively initialize child pages ArrayList<WizardPage<I,T>> subPages = page.getSubPages(); if (subPages != null) { for (int i = 0; i < subPages.size(); i++) { addAndInitializePage(subPages.get(i), false); } } } @Override protected T collectInput() { WizardPage<I,T> inputPage = activeInputPage(); if (inputPage != null) { T input = ammendInput(inputPage.collectInput()); return input; } else return null; } @Override protected void validateAsync(T input, final OperationWithInput<Boolean> onValidated) { WizardPage<I,T> inputPage = activeInputPage(); if (inputPage != null) { validating_ = true; try { inputPage.validateAsync(input, new OperationWithInput<Boolean>() { @Override public void execute(Boolean input) { validating_ = false; onValidated.execute(input); } }); } catch (Exception e) { Debug.logException(e); validating_ = false; } } else onValidated.execute(false); } @Override public void showModal() { super.showModal(); // set up state for the first page (some of this is ordinarily reached // via navigation) if (firstPage_ != null) { setOkButtonVisible(pageIsFinal(firstPage_)); firstPage_.onActivate(getProgressIndicator()); } } protected WizardPage<I,T> getFirstPage() { return firstPage_; } private WizardPage<I,T> activeInputPage() { if (activePage_ != null && !(activePage_ instanceof WizardNavigationPage<?,?>)) { return activePage_; } else { return null; } } private void animate(final Widget from, final Widget to, boolean rightToLeft, final Command onCompleted) { // protect against multiple calls if (isAnimating_) return; bodyPanel_.setWidgetVisible(to, true); int width = getOffsetWidth(); bodyPanel_.setWidgetLeftWidth(from, 0, Unit.PX, width, Unit.PX); bodyPanel_.setWidgetLeftWidth(to, rightToLeft ? width : -width, Unit.PX, width, Unit.PX); bodyPanel_.forceLayout(); bodyPanel_.setWidgetLeftWidth(from, rightToLeft ? -width : width, Unit.PX, width, Unit.PX); bodyPanel_.setWidgetLeftWidth(to, 0, Unit.PX, width, Unit.PX); isAnimating_ = true; bodyPanel_.animate(300, new AnimationCallback() { @Override public void onAnimationComplete() { bodyPanel_.setWidgetVisible(from, false); bodyPanel_.setWidgetLeftRight(to, 0, Unit.PX, 0, Unit.PX); bodyPanel_.forceLayout(); isAnimating_ = false; onCompleted.execute(); } @Override public void onLayout(Layer layer, double progress) { } }); } private void showPage(final WizardPage<I,T> page) { // ask whether the page will accept the navigation if (!page.acceptNavigation()) return; page.onBeforeActivate(new Operation() { public void execute() { // give the page the currently accumulated result, if any page.setIntermediateResult(intermediateResult_); // determine behavior based on whether this is standard page or // a navigation page final boolean okButtonVisible = pageIsFinal(page); activeParentNavigationPage_ = activePage_; activePage_.onDeactivate(new Operation() { public void execute() { animate(activePage_, page, true, new Command() { @Override public void execute() { // set active page activePage_ = page; // update header subCaptionLabel_.setVisible(false); backButton_.setVisible(true); pageCaptionLabel_.setText(page.getPageCaption()); pageCaptionLabel_.setVisible(true); // make ok button visible setOkButtonVisible(okButtonVisible); // if this is an intermediate page, make Next visible setNextButtonState(page); // let wizard and page know that the new page is active onPageActivated(page, okButtonVisible); page.onActivate(getProgressIndicator()); // set focus FocusHelper.setFocusDeferred(page); } }); } }); } }, this); } private void goBack() { final boolean isNavigationPage = activeParentNavigationPage_ != null; // determine behavior based on whether we are going back to a // navigation page or a selector page final Widget toWidget = isNavigationPage ? activeParentNavigationPage_ : firstPage_; final String pageCaptionLabel = isNavigationPage ? activeParentNavigationPage_.getPageCaption() : ""; final WizardPage<I,T> newActivePage = isNavigationPage ? activeParentNavigationPage_ : firstPage_; final CanFocus focusWidget = (CanFocus)toWidget; activeParentNavigationPage_ = null; onPageDeactivated(activePage_); activePage_.onDeactivate(new Operation() { public void execute() { animate(activePage_, toWidget, false, new Command() { @Override public void execute() { // update active page activePage_ = newActivePage; // update header subCaptionLabel_.setVisible(newActivePage == firstPage_); pageCaptionLabel_.setVisible( newActivePage != firstPage_ && isNavigationPage); pageCaptionLabel_.setText(pageCaptionLabel); setNextButtonState(newActivePage); backButton_.setVisible( newActivePage != firstPage_); // make ok button invisible setOkButtonVisible(false); // call hook onSelectorActivated(); // set focus focusWidget.focus(); } }); } }); } protected void onPageActivated(WizardPage<I,T> page, boolean okButtonVisible) { } protected void onPageDeactivated(WizardPage<I,T> page) { } protected void onSelectorActivated() { } protected T ammendInput(T input) { return input; } protected String getMainWidgetStyle() { return WizardResources.INSTANCE.styles().mainWidget(); } protected ArrayList<String> getWizardBodyStyles() { ArrayList<String> classes = new ArrayList<String>(); classes.add(WizardResources.INSTANCE.styles().wizardBodyPanel()); return classes; } private void resetOkButtonCaption() { setOkButtonCaption(okCaption_); } private boolean pageIsFinal(WizardPage<I, T> page) { return page.getSubPages() == null || page.getSubPages().size() == 0; } private void setNextButtonState(WizardPage<I, T> page) { boolean isIntermediate = page instanceof WizardIntermediatePage<?,?>; nextButton_.setVisible(isIntermediate); setDefaultOverrideButton(isIntermediate ? nextButton_ : null); } private void cleanupPage(WizardPage<I,T> page) { if (page == null) return; // notify child pages first (do cleanup in reverse order of construction) ArrayList<WizardPage<I,T>> subPages = page.getSubPages(); if (subPages != null) { for (int i = 0; i < subPages.size(); i++) { cleanupPage(subPages.get(i)); } } // clean this page page.onWizardClosing(); } private final I initialData_; private T intermediateResult_; private final String okCaption_; private LayoutPanel headerPanel_; private Label subCaptionLabel_; private Label backButton_; private Label pageCaptionLabel_; private ThemedButton nextButton_; private LayoutPanel bodyPanel_; private WizardPage<I,T> firstPage_ = null; private WizardPage<I,T> activePage_ = null; private WizardPage<I,T> activeParentNavigationPage_ = null; private boolean isAnimating_ = false; private boolean validating_ = false; }