eu.numberfour.n4js.ui.wizard.workspace.WorkspaceWizardPage.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.ui.wizard.workspace.WorkspaceWizardPage.java

Source

/**
 * Copyright (c) 2016 NumberFour AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package eu.numberfour.n4js.ui.wizard.workspace;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.keys.IBindingService;

import com.google.inject.Inject;
import com.google.inject.Provider;

import eu.numberfour.n4js.ui.ImageDescriptorCache;
import eu.numberfour.n4js.ui.dialog.ModuleSpecifierSelectionDialog;
import eu.numberfour.n4js.ui.dialog.ProjectSelectionDialog;
import eu.numberfour.n4js.ui.dialog.SourceFolderSelectionDialog;
import eu.numberfour.n4js.ui.wizard.components.WizardComponent;
import eu.numberfour.n4js.ui.wizard.components.WizardComponentContainer;
import eu.numberfour.n4js.ui.wizard.components.WizardComponentDataConverters.ConditionalConverter;
import eu.numberfour.n4js.ui.wizard.components.WizardComponentDataConverters.StringToPathConverter;
import eu.numberfour.n4js.ui.wizard.contentproposal.ModuleSpecifierContentProposalProviderFactory;
import eu.numberfour.n4js.ui.wizard.contentproposal.ModuleSpecifierContentProposalProviderFactory.ModuleSpecifierProposalLabelProvider;
import eu.numberfour.n4js.ui.wizard.contentproposal.ProjectContentProposalProvider;
import eu.numberfour.n4js.ui.wizard.contentproposal.SimpleImageContentProposalLabelProvider;
import eu.numberfour.n4js.ui.wizard.contentproposal.SourceFolderContentProposalProviderFactory;

/**
 * An abstract wizard page for {@link WorkspaceWizardModel}s.
 *
 * This page provides controls for project, source folder, module specifier. You can use {@link WizardComponent}s and
 * the {@link #createComponents(WizardComponentContainer)} methods to add additional components to your wizard.
 *
 */
public abstract class WorkspaceWizardPage<M extends WorkspaceWizardModel> extends WizardPage
        implements WizardComponentContainer {

    private static final String CONTENT_ASSIST_ECLIPSE_COMMAND_ID = "org.eclipse.ui.edit.text.contentAssist.proposals";
    private static final Image CONTENT_PROPOSAL_DECORATION_IMAGE = ImageDescriptorCache.ImageRef.SMART_LIGHTBULB
            .asImage().orNull();

    private M model;
    private DataBindingContext databindingContext;

    /** Available after invocation of #createControl */
    protected WorkspaceWizardPageForm workspaceWizardControl;

    private Image contentProposalDecorationImage;

    // Browse dialogs
    @Inject
    private Provider<ProjectSelectionDialog> projectSelectionDialogProvider;
    @Inject
    private Provider<SourceFolderSelectionDialog> sourceFolderSelectionDialogProvider;

    @Inject
    private ProjectContentProposalProvider projectContentProposalProvider;
    @Inject
    private SourceFolderContentProposalProviderFactory sourceFolderContentProviderFactory;
    @Inject
    private ModuleSpecifierContentProposalProviderFactory moduleSpecifierContentProviderFactory;

    // Content proposal adapters
    private ContentProposalAdapter sourceFolderContentProposalAdapter;
    private ContentProposalAdapter moduleSpecifierContentProposalAdapter;

    /**
     * Sole constructor.
     */
    protected WorkspaceWizardPage() {
        super(WorkspaceWizardPage.class.getName());
        this.setPageComplete(false);
    }

    @Override
    public void createControl(Composite parent) {
        workspaceWizardControl = new WorkspaceWizardPageForm(parent, SWT.FILL);

        if (null == contentProposalDecorationImage || contentProposalDecorationImage.isDisposed()) {
            contentProposalDecorationImage = CONTENT_PROPOSAL_DECORATION_IMAGE;
        }

        setupBindings(workspaceWizardControl);
        setupBrowseDialogs(workspaceWizardControl);
        setupContentProposal(workspaceWizardControl);

        createComponents(this);

        // Synchronize background color to work around a dark theme issue where the wizard content background appears
        // white
        workspaceWizardControl.setBackground(parent.getBackground());

        setControl(workspaceWizardControl);
    }

    /**
     * Open the dialog to select a module specifier
     *
     * @param shell
     *            The Shell to open the dialog in
     */
    public void openModuleSpecifierDialog(Shell shell) {
        ModuleSpecifierSelectionDialog dialog = new ModuleSpecifierSelectionDialog(shell,
                model.getProject().append(model.getSourceFolder()));

        if (!model.getModuleSpecifier().isEmpty()) {
            String initialSelectionSpecifier = model.getModuleSpecifier();

            dialog.setInitialSelection(initialSelectionSpecifier);
        }

        dialog.open();

        Object result = dialog.getFirstResult();

        if (result instanceof String) {
            IPath specifierPath = new Path((String) result);
            model.setModuleSpecifier(specifierPath.removeFileExtension().toString());
        }
    }

    /**
     * Open the dialog to select a project
     *
     * @param shell
     *            The Shell to open the dialog in
     */
    public void openProjectDialog(Shell shell) {
        ProjectSelectionDialog dialog = projectSelectionDialogProvider.get();
        dialog.open();

        Object firstResult = dialog.getFirstResult();

        if (firstResult instanceof IProject) {
            model.setProject(new Path(((IProject) firstResult).getName()));
        }
    }

    /**
     * Open the dialog to select a source folder
     *
     * @param shell
     *            The Shell to open the dialog in
     */
    public void openSourceFolderBrowseDialog(Shell shell) {
        SourceFolderSelectionDialog dialog = sourceFolderSelectionDialogProvider.get();

        // Get the IProject from the workspace. This is save as the validator ensures the project exists at this point
        IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(model.getProject().segment(0));

        dialog.setInput(project);
        dialog.setInitialSelections(new Object[] { model.getSourceFolder().removeTrailingSeparator().toString() });

        dialog.open();

        Object firstResult = dialog.getFirstResult();

        if (firstResult instanceof String) {
            model.setSourceFolder(new Path((String) firstResult));
        }
    }

    private void setupBindings(WorkspaceWizardPageForm wizardForm) {
        databindingContext = new DataBindingContext();

        WorkspaceWizardModelValidator<M> validator = getValidator();

        // Project property binding
        IObservableValue projectModelValue = BeanProperties
                .value(WorkspaceWizardModel.class, WorkspaceWizardModel.PROJECT_PROPERTY).observe(model);
        IObservableValue projectUI = WidgetProperties.text(SWT.Modify).observe(wizardForm.getProjectText());

        // Note: No model to UI conversation here as IPath is castable to String (default behavior)
        databindingContext.bindValue(projectUI, projectModelValue,
                new StringToPathConverter().updatingValueStrategy(),
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE));

        // Source folder property binding
        IObservableValue sourceFolderModelValue = BeanProperties
                .value(WorkspaceWizardModel.class, WorkspaceWizardModel.SOURCE_FOLDER_PROPERTY).observe(model);
        IObservableValue sourceFolderUI = WidgetProperties.text(SWT.Modify)
                .observe(wizardForm.getSourceFolderText());

        // Note: No model to UI conversation (see above)
        databindingContext.bindValue(sourceFolderUI, sourceFolderModelValue,
                new StringToPathConverter().updatingValueStrategy(),
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE));

        IObservableValue projectValidModelValue = BeanProperties
                .value(WorkspaceWizardModelValidator.class, WorkspaceWizardModelValidator.PROJECT_PROPERTY_VALID)
                .observe(validator);
        IObservableValue sourceFolderBrowseEnabled = WidgetProperties.enabled()
                .observe(wizardForm.getSourceFolderBrowseButton());

        databindingContext.bindValue(sourceFolderBrowseEnabled, projectValidModelValue,
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER),
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE));

        // Module specifier property binding
        IObservableValue moduleSpecifierModelValue = BeanProperties
                .value(WorkspaceWizardModel.class, WorkspaceWizardModel.MODULE_SPECIFIER_PROPERTY).observe(model);
        IObservableValue moduleSpecifierUI = BeanProperties.value(SuffixText.class, SuffixText.TEXT_PROPERTY)
                .observe(wizardForm.getModuleSpecifierText());
        databindingContext.bindValue(moduleSpecifierUI, moduleSpecifierModelValue);

        // Conditional activation of the browse buttons according to the precedent input fields validity
        IObservableValue moduleSpecifierBrowseEnabled = WidgetProperties.enabled()
                .observe(wizardForm.getModuleSpecifierBrowseButton());
        IObservableValue sourceFolderValidValue = BeanProperties.value(WorkspaceWizardModelValidator.class,
                WorkspaceWizardModelValidator.SOURCE_FOLDER_PROPERTY_VALID).observe(validator);
        IObservableValue projectValidValue = BeanProperties
                .value(WorkspaceWizardModelValidator.class, WorkspaceWizardModelValidator.PROJECT_PROPERTY_VALID)
                .observe(validator);

        ConditionalConverter moduleSpecifierBrowseableConverter = new ConditionalConverter() {
            @Override
            public boolean validate(Object object) {
                return validator.getSourceFolderValid() && validator.getProjectValid();
            }
        };

        // Bind model changes of project or source folder property to the enabled state of the module specifier browse
        // button.
        databindingContext.bindValue(moduleSpecifierBrowseEnabled, projectValidValue,
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER),
                moduleSpecifierBrowseableConverter.updatingValueStrategy());
        databindingContext.bindValue(moduleSpecifierBrowseEnabled, sourceFolderValidValue,
                new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER),
                moduleSpecifierBrowseableConverter.updatingValueStrategy());
    }

    private void setupBrowseDialogs(WorkspaceWizardPageForm wizardForm) {
        wizardForm.getProjectBrowseButton().addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                openProjectDialog(wizardForm.getShell());
            }
        });

        wizardForm.getSourceFolderBrowseButton().addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                openSourceFolderBrowseDialog(wizardForm.getShell());
            }
        });

        wizardForm.getModuleSpecifierBrowseButton().addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                openModuleSpecifierDialog(wizardForm.getShell());
            }
        });
    }

    private void setupContentProposal(WorkspaceWizardPageForm wizardForm) {
        // Get the active binding's content assist key strokes
        KeyStroke keyInitiator = getActiveContentAssistBinding();

        // If unbound don't configure the content proposal
        if (null == keyInitiator) {
            return;
        }

        // Setup project content proposal
        ContentProposalAdapter projectAdapter = new ContentProposalAdapter(wizardForm.getProjectText(),
                new TextContentAdapter(), projectContentProposalProvider, keyInitiator, null);
        projectAdapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);

        ImageDescriptor projectSymbol = PlatformUI.getWorkbench().getSharedImages()
                .getImageDescriptor(IDE.SharedImages.IMG_OBJ_PROJECT);
        projectAdapter.setLabelProvider(new SimpleImageContentProposalLabelProvider(projectSymbol));

        createContentProposalDecoration(wizardForm.getProjectText());

        sourceFolderContentProposalAdapter = new ContentProposalAdapter(wizardForm.getSourceFolderText(),
                new TextContentAdapter(), null, keyInitiator, null);
        sourceFolderContentProposalAdapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);

        sourceFolderContentProposalAdapter.setLabelProvider(new SimpleImageContentProposalLabelProvider(
                ImageDescriptorCache.ImageRef.SRC_FOLDER.asImageDescriptor().orNull()));

        createContentProposalDecoration(wizardForm.getSourceFolderText());

        moduleSpecifierContentProposalAdapter = new ContentProposalAdapter(
                wizardForm.getModuleSpecifierText().getInternalText(), new TextContentAdapter(), null, keyInitiator,
                null);

        wizardForm.getModuleSpecifierText().createDecoration(contentProposalDecorationImage);

        // Update proposal context whenever the model changes
        model.addPropertyChangeListener(evt -> {
            if (evt.getPropertyName() == WorkspaceWizardModel.PROJECT_PROPERTY
                    || evt.getPropertyName() == WorkspaceWizardModel.SOURCE_FOLDER_PROPERTY) {
                updateProposalContext();
            }
        });
        updateProposalContext();

        moduleSpecifierContentProposalAdapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
        moduleSpecifierContentProposalAdapter.setLabelProvider(new ModuleSpecifierProposalLabelProvider());
    }

    /**
     * Returns the active key binding for content assist.
     *
     * If no binding is set null is returned.
     */
    private KeyStroke getActiveContentAssistBinding() {
        IBindingService bindingService = PlatformUI.getWorkbench().getService(IBindingService.class);
        TriggerSequence[] activeBindingsFor = bindingService
                .getActiveBindingsFor(CONTENT_ASSIST_ECLIPSE_COMMAND_ID);

        if (activeBindingsFor.length > 0 && activeBindingsFor[0] instanceof KeySequence) {
            KeyStroke[] strokes = ((KeySequence) activeBindingsFor[0]).getKeyStrokes();
            if (strokes.length == 1) {
                return strokes[0];
            }
        }
        return null;
    }

    /**
     * Adds the content proposal field decoration to a given control.
     *
     * @param control
     *            The control to decorate
     */
    private ControlDecoration createContentProposalDecoration(Control control) {
        ControlDecoration decoration = new ControlDecoration(control, SWT.TOP | SWT.LEFT);
        decoration.setImage(contentProposalDecorationImage);
        decoration.setShowOnlyOnFocus(true);
        return decoration;
    }

    /**
     * This method should be invoked whenever source folder or project value change, to update the proposal contexts for
     * the field source folder and module specifier
     */
    private void updateProposalContext() {
        IPath projectPath = model.getProject();
        IPath sourceFolderPath = model.getSourceFolder();

        // Early exit for empty project value
        if (projectPath.isEmpty()) {
            sourceFolderContentProposalAdapter.setContentProposalProvider(null);
            moduleSpecifierContentProposalAdapter.setContentProposalProvider(null);
            return;
        }

        IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectPath.toString());

        if (null == project || !project.exists()) {
            // Disable source folder and module specifier proposals
            sourceFolderContentProposalAdapter.setContentProposalProvider(null);
            moduleSpecifierContentProposalAdapter.setContentProposalProvider(null);
        } else {
            // Try to retrieve the source folder and if not specified set it to null
            IContainer sourceFolder = sourceFolderPath.segmentCount() != 0 ? project.getFolder(sourceFolderPath)
                    : null;

            // If the project exists, enable source folder proposals
            sourceFolderContentProposalAdapter.setContentProposalProvider(
                    sourceFolderContentProviderFactory.createProviderForProject(project));

            if (null != sourceFolder && sourceFolder.exists()) {
                // If source folder exists as well enable module specifier proposal
                moduleSpecifierContentProposalAdapter.setContentProposalProvider(
                        moduleSpecifierContentProviderFactory.createProviderForPath(sourceFolder.getFullPath()));
            } else {
                // Otherwise disable module specifier proposals
                moduleSpecifierContentProposalAdapter.setContentProposalProvider(null);
            }
        }
    }

    /**
     * @param model
     *            WorkspaceWizardModel to use
     */
    public void setModel(M model) {
        this.model = model;
        getValidator().setModel(this.model);
        this.model.addPropertyChangeListener(evt -> getValidator().validate());
    }

    /**
     * Returns with the underlying module instance.
     *
     * @return the model instance used for data binding.
     */
    public M getModel() {
        return model;
    }

    /**
     * Set the initial focus when the pages is visible
     */
    @Override
    public void setVisible(boolean visible) {
        super.setVisible(visible);

        if (visible) {
            this.setInitialFocus();
        }

    }

    /**
     * Set the input focus depending on the initially given model.
     *
     * This method is only invoked when all ui is initialized and visible.
     *
     * @return True if the focus was claimed.
     */
    protected boolean setInitialFocus() {
        // Set the focus to the first empty field beginning with project
        if (model.getProject().toString().isEmpty()) {
            workspaceWizardControl.getProjectText().setFocus();
        } else if (model.getSourceFolder().toString().isEmpty()) {
            workspaceWizardControl.getSourceFolderText().setFocus();
        } else {
            return false;
        }
        return true;
    }

    @Override
    public DataBindingContext getDataBindingContext() {
        return databindingContext;
    }

    @Override
    public void dispose() {
        super.dispose();
        if (databindingContext != null) {
            databindingContext.dispose();
        }
    }

    @Override
    public Composite getComposite() {
        return workspaceWizardControl;
    }

    /**
     * Get the validator for this wizard page.
     *
     * Subclasses need to provide their own ModelValidator through this method and configure it to work with the model.
     *
     * @return A {@link WorkspaceWizardModelValidator}
     */
    public abstract WorkspaceWizardModelValidator<M> getValidator();

    /**
     * Implement this method to add custom form components.
     * <p>
     * Note that the parent composite is a 3 column grid.
     * </p>
     *
     * @param parent
     *            The parent composite
     */
    public abstract void createComponents(WizardComponentContainer parent);

}