// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.debug.ui.liveedit; import static org.chromium.debug.ui.DialogUtils.createConstant; import static org.chromium.debug.ui.DialogUtils.createErrorOptional; import static org.chromium.debug.ui.DialogUtils.createOptional; import static org.chromium.debug.ui.DialogUtils.createProcessor; import static org.chromium.debug.ui.DialogUtils.handleErrors; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import org.chromium.debug.core.ChromiumDebugPlugin; import org.chromium.debug.core.model.PushChangesPlan; import org.chromium.debug.core.util.ScriptTargetMapping; import org.chromium.debug.ui.DialogUtils; import org.chromium.debug.ui.DialogUtils.BranchVariableGetter; import org.chromium.debug.ui.DialogUtils.Gettable; import org.chromium.debug.ui.DialogUtils.Message; import org.chromium.debug.ui.DialogUtils.MessagePriority; import org.chromium.debug.ui.DialogUtils.NormalExpression; import org.chromium.debug.ui.DialogUtils.Optional; import org.chromium.debug.ui.DialogUtils.OptionalSwitcher; import org.chromium.debug.ui.DialogUtils.Scope; import org.chromium.debug.ui.DialogUtils.ScopeEnabler; import org.chromium.debug.ui.DialogUtils.Updater; import org.chromium.debug.ui.DialogUtils.ValueConsumer; import org.chromium.debug.ui.DialogUtils.ValueProcessor; import org.chromium.debug.ui.DialogUtils.ValueSource; import org.chromium.debug.ui.WizardUtils.LogicBasedWizard; import org.chromium.debug.ui.WizardUtils.NextPageEnabler; import org.chromium.debug.ui.WizardUtils.PageElements; import org.chromium.debug.ui.WizardUtils.PageImpl; import org.chromium.debug.ui.WizardUtils.PageListener; import org.chromium.debug.ui.WizardUtils.WizardFinishController; import org.chromium.debug.ui.WizardUtils.WizardFinisher; import org.chromium.debug.ui.WizardUtils.WizardLogic; import org.chromium.debug.ui.actions.ChooseVmControl; import org.chromium.debug.ui.liveedit.PushChangesWizard.FinisherDelegate; import org.chromium.sdk.UpdatableScript.ChangeDescription; /** * Creates Updater-based logic implementation of the wizard. It is responsible for proper data * manipulation and view control updates. * <p> * The wizard pages are arranged in graph with one fork:<br> * 'choose vm' -> * <ul> * <li>[single vm path] -> 'textual preview' -> 'v8 preview' * <li>[multiple vm path] -> 'multiple vm stub' * </ul> */ class WizardLogicBuilder { private final Updater updater; private final PushChangesWizard.PageSet pageSet; private final LogicBasedWizard wizardImpl; WizardLogicBuilder(PushChangesWizard.PageSet pageSet, LogicBasedWizard wizardImpl) { this.pageSet = pageSet; this.wizardImpl = wizardImpl; updater = new Updater(); } WizardLogic create(final List<? extends ScriptTargetMapping> targetList) { Scope scope = updater.rootScope(); final boolean skipSingleTargetSelection = true; // Wizard logic is described from the first page toward the last pages. final PageImpl<PushChangesWizard.ChooseVmPageElements> chooseVmPage = pageSet.getChooseVmPage(); // A value corresponding to selected VMs on 'choose vm' page. final ValueSource<List<ScriptTargetMapping>> selectedVmInput = new ValueSource<List<ScriptTargetMapping>>() { private final ChooseVmControl.Logic chooseVmControl = chooseVmPage.getPageElements().getChooseVm(); { chooseVmControl.setData(targetList); chooseVmControl.selectAll(); final ValueSource<?> thisSource = this; ChooseVmControl.Logic.Listener listener = new ChooseVmControl.Logic.Listener() { public void checkStateChanged() { updater.reportChanged(thisSource); updater.update(); } }; chooseVmControl.addListener(listener); } public List<ScriptTargetMapping> getValue() { return chooseVmControl.getSelected(); } }; updater.addSource(scope, selectedVmInput); final ValueProcessor<? extends List<Optional<PushChangesPlan>>> selectedChangePlansValue = createProcessor(new Gettable<List<Optional<PushChangesPlan>>>() { @Override public List<Optional<PushChangesPlan>> getValue() { List<ScriptTargetMapping> input = selectedVmInput.getValue(); List<Optional<PushChangesPlan>> result = new ArrayList<DialogUtils.Optional<PushChangesPlan>>(input.size()); for (ScriptTargetMapping mapping : input) { Optional<PushChangesPlan> optionalPlan; try { PushChangesPlan plan = PushChangesPlan.create(mapping); optionalPlan = createOptional(plan); } catch (RuntimeException e) { // TODO: have more specific exception types to catch. optionalPlan = createErrorOptional(new Message( "Failed to get script source from a file. See log for details.", MessagePriority.BLOCKING_PROBLEM)); } result.add(optionalPlan); } return result; } }); updater.addSource(scope, selectedChangePlansValue); updater.addConsumer(scope, selectedChangePlansValue); updater.addDependency(selectedChangePlansValue, selectedVmInput); // A derived value of selected VMs list; the list is non-empty or the value is error. final ValueProcessor<? extends Optional<List<PushChangesPlan>>> nonEmptySelectedPlansValue = createProcessor(new Gettable<Optional<List<PushChangesPlan>>>() { public Optional<List<PushChangesPlan>> getValue() { List<Optional<PushChangesPlan>> planList = selectedChangePlansValue.getValue(); if (planList.isEmpty()) { return createErrorOptional( new Message("Choose at least one VM", MessagePriority.BLOCKING_INFO)); } List<Message> errorMessages = new LinkedList<Message>(); List<PushChangesPlan> result = new ArrayList<PushChangesPlan>(planList.size()); for (Optional<PushChangesPlan> optionalPlan : planList) { if (optionalPlan.isNormal()) { result.add(optionalPlan.getNormal()); } else { errorMessages.addAll(optionalPlan.errorMessages()); } } if (errorMessages.isEmpty()) { return createOptional(result); } else { return createErrorOptional(new HashSet<Message>(errorMessages)); } } }); updater.addSource(scope, nonEmptySelectedPlansValue); updater.addConsumer(scope, nonEmptySelectedPlansValue); updater.addDependency(nonEmptySelectedPlansValue, selectedChangePlansValue); // A condition value for up-coming fork between 'single vm' and 'multiple vm' paths. Gettable<? extends Optional<? extends Boolean>> singleVmSelectedExpression = handleErrors( new NormalExpression<Boolean>() { @Calculate public Boolean calculate(List<PushChangesPlan> selectedVm) { return selectedVm.size() == 1; } @DependencyGetter public ValueSource<? extends Optional<List<PushChangesPlan>>> getSelectVmSource() { return nonEmptySelectedPlansValue; } }); // A switch between 2 paths: 'single vm' and 'multiple vm'. OptionalSwitcher<Boolean> singleVmSelectedSwitch = scope.addOptionalSwitch(singleVmSelectedExpression); final PreviewAndOptionPath singleVmPath = createSingleVmPath(chooseVmPage, singleVmSelectedSwitch, nonEmptySelectedPlansValue); final PreviewAndOptionPath multipleVmPath = createMultipleVmPath(chooseVmPage, singleVmSelectedSwitch, nonEmptySelectedPlansValue); final PreviewAndOptionPath switchBlockItems = DialogUtils.mergeBranchVariables( PreviewAndOptionPath.class, singleVmSelectedSwitch, singleVmPath, multipleVmPath); // A simple value converter that wraps wizard delegate as UI-aware wizard finisher. ValueProcessor<Optional<? extends WizardFinisher>> finisherValue = createProcessor(handleErrors(new NormalExpression<WizardFinisher>() { @Calculate public WizardFinisher calculate(FinisherDelegate finisherDelegate) { return new PushChangesWizard.FinisherImpl(finisherDelegate); } @DependencyGetter public ValueSource<? extends Optional<? extends FinisherDelegate>> getWizardFinisherDelegateSource() { return switchBlockItems.getFinisherDelegateValue(); } })); updater.addConsumer(scope, finisherValue); updater.addSource(scope, finisherValue); updater.addDependency(finisherValue, switchBlockItems.getFinisherDelegateValue()); // A controller that ties finisher value and other warnings to a wizard UI. WizardFinishController finishController = new WizardFinishController(finisherValue, switchBlockItems.getWarningValue(), wizardImpl); updater.addConsumer(scope, finishController); updater.addDependency(finishController, switchBlockItems.getFinisherDelegateValue()); updater.addDependency(finishController, switchBlockItems.getWarningValue()); return new WizardLogic() { public void updateAll() { updater.updateAll(); } public PageImpl<?> getStartingPage() { return chooseVmPage; } public void dispose() { updater.stopAsync(); } }; } /** * An internal interface that defines a uniform output of preview path. The preview path * is responsible for returning finisher (which may carry error messages) and it also may * return additional warning messages. */ private interface PreviewAndOptionPath { @BranchVariableGetter ValueSource<? extends Optional<? extends FinisherDelegate>> getFinisherDelegateValue(); @BranchVariableGetter ValueSource<Optional<Void>> getWarningValue(); } /** * Creates a 'single vm' page path in wizard. User sees it after choosing exactly one VM. * It consists of 2 pages, 'textual preview' and 'v8 preview'. */ private PreviewAndOptionPath createSingleVmPath(PageImpl<?> basePage, OptionalSwitcher<Boolean> switcher, final ValueSource<? extends Optional<? extends List<PushChangesPlan>>> selectedVmValue) { // This path consists of 1 page final PageImpl<PushChangesWizard.V8PreviewPageElements> v8PreviewPage = pageSet.getV8PreviewPage(); // All logic is inside a dedicated scope, which gets enabled only when user chooses exactly // one VM on a previous page. The scope enablement is synchronized with these pages becoming // available to user. ScopeEnabler scopeEnabler = new NextPageEnabler(basePage, v8PreviewPage); Scope scope = switcher.addScope(Boolean.TRUE, scopeEnabler); // A value of the single vm, that must be always available within this scope. final ValueProcessor<PushChangesPlan> singlePlanValue = createProcessor(new Gettable<PushChangesPlan>() { public PushChangesPlan getValue() { // Value targets should be normal (by switcher condition). return selectedVmValue.getValue().getNormal().get(0); } }); updater.addConsumer(scope, singlePlanValue); updater.addSource(scope, singlePlanValue); updater.addDependency(singlePlanValue, selectedVmValue); // A complex asynchronous value source that feeds update preview data from V8. // The data is in raw format. final PreviewLoader previewRawResultValue = new PreviewLoader(updater, singlePlanValue); previewRawResultValue.registerSelf(scope); // previewRawResultValue is trigged only when page is actually visible to user. v8PreviewPage.addListener(new PageListener() { public void onSetVisible(boolean visible) { previewRawResultValue.setActive(visible); } }); // Parses raw preview value and converts it into a form suitable for the viewer; also handles // errors that become warnings. final ValueProcessor<Optional<? extends LiveEditDiffViewer.Input>> previewValue = createProcessor(handleErrors( new NormalExpression<LiveEditDiffViewer.Input>() { @Calculate public Optional<? extends LiveEditDiffViewer.Input> calculate( PreviewLoader.Data previewRawResultParam) { PushChangesPlan changesPlan = singlePlanValue.getValue(); ChangeDescription changeDescription = previewRawResultParam.getChangeDescription(); Optional<LiveEditDiffViewer.Input> result; if (changeDescription == null) { result = createOptional(null); } else { try { LiveEditDiffViewer.Input viewerInput = PushResultParser.createViewerInput(changeDescription, changesPlan, true); result = createOptional(viewerInput); } catch (RuntimeException e) { ChromiumDebugPlugin.log(e); result = createErrorOptional(new Message( "Error in getting preview: " + e.toString(), MessagePriority.WARNING)); } } return result; } @DependencyGetter public ValueSource<Optional<PreviewLoader.Data>> previewRawResultValueSource() { return previewRawResultValue; } })); updater.addConsumer(scope, previewValue); updater.addSource(scope, previewValue); updater.addDependency(previewValue, previewRawResultValue); updater.addDependency(previewValue, singlePlanValue); // A simple consumer that sets preview data to the viewer. ValueConsumer v8PreviewInputSetter = new ValueConsumer() { public void update(Updater updater) { Optional<? extends LiveEditDiffViewer.Input> previewOptional = previewValue.getValue(); LiveEditDiffViewer.Input viewerInput; if (previewOptional.isNormal()) { viewerInput = previewOptional.getNormal(); } else { viewerInput = null; } v8PreviewPage.getPageElements().getPreviewViewer().setInput(viewerInput); } }; updater.addConsumer(scope, v8PreviewInputSetter); updater.addDependency(v8PreviewInputSetter, previewValue); // A warning generator that collects them from v8 preview loader. final ValueProcessor<Optional<Void>> warningValue = createProcessor( new Gettable<Optional<Void>>() { public Optional<Void> getValue() { Optional<?> previewResult = previewValue.getValue(); if (previewResult.isNormal()) { return createOptional(null); } else { return createErrorOptional(previewResult.errorMessages()); } } }); updater.addConsumer(scope, warningValue); updater.addSource(scope, warningValue); updater.addDependency(warningValue, previewValue); // A finisher delegate source, that does not actually depend on most of the code above. final ValueProcessor<? extends Optional<FinisherDelegate>> wizardFinisher = createProcessor(( new Gettable<Optional<FinisherDelegate>>() { public Optional<FinisherDelegate> getValue() { FinisherDelegate finisher = new PushChangesWizard.SingleVmFinisher(singlePlanValue.getValue()); return createOptional(finisher); } })); updater.addSource(scope, wizardFinisher); updater.addConsumer(scope, wizardFinisher); updater.addDependency(wizardFinisher, singlePlanValue); return new PreviewAndOptionPath() { public ValueSource<? extends Optional<FinisherDelegate>> getFinisherDelegateValue() { return wizardFinisher; } public ValueSource<Optional<Void>> getWarningValue() { return warningValue; } }; } /** * Creates a 'multiple vm' page path in wizard that is not too reach at this moment. * User basically only sees a stub label. We do invest too much into the case when several * target VMs are selected. */ private PreviewAndOptionPath createMultipleVmPath(PageImpl<?> basePage, OptionalSwitcher<Boolean> switcher, final ValueSource<? extends Optional<? extends List<PushChangesPlan>>> selectedVmValue) { PageImpl<PageElements> multipleVmStubPage = pageSet.getMultipleVmStubPage(); ScopeEnabler scopeEnabler = new NextPageEnabler(basePage, multipleVmStubPage); Scope scope = switcher.addScope(Boolean.FALSE, scopeEnabler); final ValueProcessor<Optional<? extends FinisherDelegate>> wizardFinisher = createProcessor(handleErrors(new NormalExpression<FinisherDelegate>() { @Calculate public FinisherDelegate calculate(List<PushChangesPlan> selectedVm) { return new PushChangesWizard.MultipleVmFinisher(selectedVmValue.getValue().getNormal()); } @DependencyGetter public ValueSource<? extends Optional<? extends List<PushChangesPlan>>> getSelectVmSource() { return selectedVmValue; } })); updater.addSource(scope, wizardFinisher); updater.addConsumer(scope, wizardFinisher); updater.addDependency(wizardFinisher, selectedVmValue); final ValueSource<Optional<Void>> warningValue = createConstant(createOptional((Void) null), updater); return new PreviewAndOptionPath() { public ValueSource<? extends Optional<? extends FinisherDelegate>> getFinisherDelegateValue() { return wizardFinisher; } public ValueSource<Optional<Void>> getWarningValue() { return warningValue; } }; } }