package org.bndtools.core.ui.wizards.shared;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.bndtools.templating.Category;
import org.bndtools.templating.Template;
import org.bndtools.templating.TemplateLoader;
import org.bndtools.utils.jface.ProgressRunner;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.wizard.IWizardPage;
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.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.forms.widgets.ScrolledFormText;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentConstants;
import org.osgi.util.promise.Promise;
import aQute.bnd.osgi.Processor;
import aQute.lib.io.IO;
import aQute.libg.tuple.Pair;
import bndtools.Plugin;
public class TemplateSelectionWizardPage extends WizardPage {
public static final String PROP_TEMPLATE = "template";
private static final String NO_HELP_CONTENT = "<form>No help content available</form>";
private final ILog log = Plugin.getDefault().getLog();
private final PropertyChangeSupport propSupport = new PropertyChangeSupport(this);
private final String templateType;
private final Template emptyTemplate;
private final Map<Template,Image> loadedImages = new IdentityHashMap<>();
private Tree tree;
private TreeViewer viewer;
private RepoTemplateContentProvider contentProvider;
private final LatestTemplateFilter latestFilter = new LatestTemplateFilter();
private Button btnLatestOnly;
private ScrolledFormText txtDescription;
private Image defaultTemplateImage;
private Template selected = null;
private boolean shown = false;
public TemplateSelectionWizardPage(String pageName, String templateType, Template emptyTemplate) {
super(pageName);
this.templateType = templateType;
this.emptyTemplate = emptyTemplate;
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible && !shown) {
shown = true;
loadTemplates();
}
}
/*
* Don't allow wizard to complete before this page is shown
*/
@Override
public boolean isPageComplete() {
return shown && selected != null && super.isPageComplete();
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propSupport.removePropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propSupport.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propSupport.removePropertyChangeListener(propertyName, listener);
}
@Override
public void createControl(Composite parent) {
setImageDescriptor(Plugin.imageDescriptorFromPlugin("icons/bndtools-wizban.png")); //$NON-NLS-1$
GridData gd;
Composite composite = new Composite(parent, SWT.NULL);
setControl(composite);
composite.setLayout(new GridLayout(1, false));
Control headerControl = createHeaderControl(composite);
if (headerControl != null)
headerControl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
new Label(composite, SWT.NONE).setText("Select Template:");
tree = new Tree(composite, SWT.BORDER | SWT.FULL_SELECTION);
gd = new GridData(SWT.FILL, SWT.FILL, true, true);
gd.heightHint = 150;
tree.setLayoutData(gd);
defaultTemplateImage = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "icons/template.gif").createImage(parent.getDisplay());
viewer = new TreeViewer(tree);
contentProvider = new RepoTemplateContentProvider(false);
viewer.setContentProvider(contentProvider);
viewer.setLabelProvider(new RepoTemplateLabelProvider(loadedImages, defaultTemplateImage));
viewer.addFilter(latestFilter);
setTemplates(emptyTemplate != null ? Collections.singletonList(emptyTemplate) : Collections.<Template> emptyList());
btnLatestOnly = new Button(composite, SWT.CHECK);
btnLatestOnly.setText("Show latest versions only");
btnLatestOnly.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));
btnLatestOnly.setSelection(true);
new Label(composite, SWT.NONE).setText("Description:");
Composite cmpDescription = new Composite(composite, SWT.BORDER);
cmpDescription.setBackground(tree.getBackground());
txtDescription = new ScrolledFormText(cmpDescription, SWT.V_SCROLL | SWT.H_SCROLL, false);
FormText formText = new FormText(txtDescription, SWT.NO_FOCUS);
txtDescription.setFormText(formText);
txtDescription.setBackground(tree.getBackground());
formText.setBackground(tree.getBackground());
formText.setForeground(tree.getForeground());
formText.setFont("fixed", JFaceResources.getTextFont());
formText.setFont("italic", JFaceResources.getFontRegistry().getItalic(""));
GridData gd_cmpDescription = new GridData(SWT.FILL, SWT.FILL, true, true);
gd_cmpDescription.heightHint = 25;
cmpDescription.setLayoutData(gd_cmpDescription);
GridLayout layout_cmpDescription = new GridLayout(1, false);
cmpDescription.setLayout(layout_cmpDescription);
GridData gd_txtDescription = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1);
gd_txtDescription.heightHint = 25;
txtDescription.setLayoutData(gd_txtDescription);
Hyperlink linkRetina = new Hyperlink(composite, SWT.NONE);
linkRetina.setText("Why is this text blurred?");
linkRetina.setUnderlined(true);
linkRetina.setForeground(JFaceColors.getHyperlinkText(getShell().getDisplay()));
linkRetina.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));
viewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
setTemplate(element instanceof Template ? (Template) element : null);
getContainer().updateButtons();
}
});
viewer.addOpenListener(new IOpenListener() {
@Override
public void open(OpenEvent event) {
Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
setTemplate(element instanceof Template ? (Template) element : null);
getContainer().updateButtons();
IWizardPage nextPage = getNextPage();
if (nextPage != null && selected != null)
getContainer().showPage(nextPage);
}
});
btnLatestOnly.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean latestOnly = btnLatestOnly.getSelection();
if (latestOnly)
viewer.addFilter(latestFilter);
else
viewer.removeFilter(latestFilter);
}
});
linkRetina.addHyperlinkListener(new HyperlinkAdapter() {
@Override
public void linkActivated(HyperlinkEvent ev) {
try {
IWorkbenchBrowserSupport browser = PlatformUI.getWorkbench().getBrowserSupport();
browser.getExternalBrowser().openURL(new URL("https://github.com/bndtools/bndtools/wiki/Blurry-Form-Text-on-High-Resolution-Displays"));
} catch (Exception e) {
log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Browser open error", e));
}
}
});
txtDescription.getFormText().addHyperlinkListener(new HyperlinkAdapter() {
@Override
public void linkActivated(HyperlinkEvent ev) {
try {
PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL((String) ev.getHref()));
} catch (Exception ex) {
log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Browser open error", ex));
}
}
});
}
/**
* Can be overridden to provide a control that will be placed at the top of the page.
*/
protected Control createHeaderControl(@SuppressWarnings("unused") Composite parent) {
return null;
}
private class LoadTemplatesJob implements IRunnableWithProgress {
private final Shell shell;
private final String originalMessage;
private final BundleContext context = FrameworkUtil.getBundle(LoadTemplatesJob.class).getBundleContext();
public LoadTemplatesJob(Shell shell, String originalMessage) {
this.shell = shell;
this.originalMessage = originalMessage;
}
@Override
public void run(IProgressMonitor progress) throws InvocationTargetException {
SubMonitor monitor = SubMonitor.convert(progress);
try {
final Set<Template> templates = new LinkedHashSet<>();
// Fire all the template loaders and get their promises
List<ServiceReference<TemplateLoader>> templateLoaderSvcRefs = new ArrayList<>(context.getServiceReferences(TemplateLoader.class, null));
monitor.beginTask("Loading templates...", templateLoaderSvcRefs.size());
Collections.sort(templateLoaderSvcRefs);
List<Pair<String,Promise< ? extends Collection<Template>>>> promises = new LinkedList<>();
for (ServiceReference<TemplateLoader> templateLoaderSvcRef : templateLoaderSvcRefs) {
String label = (String) templateLoaderSvcRef.getProperty(Constants.SERVICE_DESCRIPTION);
if (label == null)
label = (String) templateLoaderSvcRef.getProperty(ComponentConstants.COMPONENT_NAME);
if (label == null)
label = String.format("Template Loader service ID " + templateLoaderSvcRef.getProperty(Constants.SERVICE_ID));
TemplateLoader templateLoader = context.getService(templateLoaderSvcRef);
try {
Promise< ? extends Collection<Template>> promise = templateLoader.findTemplates(templateType, new Processor());
promises.add(new Pair<String,Promise< ? extends Collection<Template>>>(label, promise));
} finally {
context.ungetService(templateLoaderSvcRef);
}
}
// Force the promises in sequence
for (Pair<String,Promise< ? extends Collection<Template>>> namedPromise : promises) {
String name = namedPromise.getFirst();
SubMonitor childMonitor = monitor.newChild(1, SubMonitor.SUPPRESS_NONE);
childMonitor.beginTask(name, 1);
try {
Throwable failure = namedPromise.getSecond().getFailure();
if (failure != null)
Plugin.getDefault().getLog().log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Failed to load from template loader: " + name, failure));
else {
Collection<Template> loadedTemplates = namedPromise.getSecond().getValue();
templates.addAll(loadedTemplates);
}
} catch (InterruptedException e) {
Plugin.getDefault().getLog().log(new Status(IStatus.WARNING, Plugin.PLUGIN_ID, 0, "Interrupted while loading from template loader: " + name, e));
}
}
// Add empty template if provided
if (emptyTemplate != null)
templates.add(emptyTemplate);
// Display results
Control control = viewer.getControl();
if (control != null && !control.isDisposed()) {
control.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
setTemplates(templates);
IconLoaderJob iconLoaderJob = new IconLoaderJob(templates, viewer, loadedImages, 5);
iconLoaderJob.setSystem(true);
iconLoaderJob.schedule(0);
}
});
}
} catch (InvalidSyntaxException ex) {
throw new InvocationTargetException(ex);
} finally {
// Restore the original message to the page
if (!shell.isDisposed())
shell.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
setMessage(originalMessage);
}
});
if (progress != null)
progress.done();
}
}
}
protected void loadTemplates() {
final String oldMessage = getMessage();
final Shell shell = getShell();
setMessage("Loading templates...");
try {
ProgressRunner.execute(true, new LoadTemplatesJob(shell, oldMessage), getContainer(), getContainer().getShell().getDisplay());
} catch (InvocationTargetException e) {
Throwable exception = e.getTargetException();
ErrorDialog.openError(getShell(), "Error", null, new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error loading templates.", exception));
}
}
@Override
public void dispose() {
super.dispose();
if (!defaultTemplateImage.isDisposed())
defaultTemplateImage.dispose();
for (Entry<Template,Image> entry : loadedImages.entrySet()) {
Image img = entry.getValue();
if (!img.isDisposed())
img.dispose();
}
if (selected != null) {
try {
selected.close();
} catch (IOException e) {
Plugin.getDefault().getLog().log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Problem cleaning up template content", e));
}
}
}
private void setTemplates(final Collection<Template> templates) {
viewer.setInput(templates);
viewer.expandAll();
Template templateToSelect = null;
if (viewer.getFilters().length == 0) {
templateToSelect = contentProvider.getFirstTemplate();
} else {
for (Object element : contentProvider.getElements(null)) {
if (element instanceof Category) {
Object[] filteredTemplates = latestFilter.filter(viewer, element, contentProvider.getChildren(element));
if (filteredTemplates.length > 0) {
templateToSelect = (Template) filteredTemplates[0];
break;
}
} else {
templateToSelect = (Template) element;
break;
}
}
}
if (templateToSelect == null) {
return;
}
viewer.setSelection(new StructuredSelection(templateToSelect));
}
public void setTemplate(final Template template) {
Template old = this.selected;
this.selected = template;
propSupport.firePropertyChange(PROP_TEMPLATE, old, template);
if (template != null) {
txtDescription.setText(String.format("<form>Loading help content for template '%s'...</form>", template.getName()));
Job updateDescJob = new UpdateDescriptionJob(template, txtDescription);
updateDescJob.setSystem(true);
updateDescJob.schedule();
} else {
txtDescription.setText(NO_HELP_CONTENT);
}
}
public Template getTemplate() {
return selected;
}
private final class UpdateDescriptionJob extends Job {
private final Template template;
private final ScrolledFormText control;
private UpdateDescriptionJob(Template template, ScrolledFormText control) {
super("update description");
this.template = template;
this.control = control;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
String tmp = NO_HELP_CONTENT;
if (template != null) {
URI uri = template.getHelpContent();
if (uri != null) {
try {
URLConnection conn = uri.toURL().openConnection();
conn.setUseCaches(false);
tmp = IO.collect(conn.getInputStream());
} catch (IOException e) {
log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error loading template help content.", e));
}
}
}
final String text = tmp;
if (control != null && !control.isDisposed()) {
control.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!control.isDisposed())
control.setText(text);
}
});
}
return Status.OK_STATUS;
}
}
}