/******************************************************************************* * Copyright (c) 2008-2015 Sonatype, Inc. and others * 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: * Sonatype, Inc. - initial API and implementation * Anton Tanasenko - Refactor marker resolutions and quick fixes (Bug #484359) *******************************************************************************/ package org.eclipse.m2e.editor.xml.internal; import java.util.ArrayList; import java.util.List; import org.eclipse.core.resources.IMarker; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.AbstractInformationControl; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IInformationControlExtension3; import org.eclipse.jface.text.IInformationControlExtension5; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IRewriteTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; import org.eclipse.jface.viewers.StyledString; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IMarkerResolution; import org.eclipse.ui.IMarkerResolution2; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.editors.text.EditorsUI; import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess; import org.eclipse.m2e.core.ui.internal.markers.MavenProblemResolution; import org.eclipse.m2e.editor.xml.MvnImages; import org.eclipse.m2e.editor.xml.PomHyperlinkDetector; import org.eclipse.m2e.editor.xml.PomHyperlinkDetector.ExpressionRegion; import org.eclipse.m2e.editor.xml.PomHyperlinkDetector.ManagedArtifactRegion; import org.eclipse.m2e.editor.xml.PomHyperlinkDetector.MarkerRegion; import org.eclipse.m2e.editor.xml.PomTextHover; import org.eclipse.m2e.editor.xml.PomTextHover.CompoundRegion; import org.eclipse.m2e.internal.discovery.markers.MavenDiscoveryMarkerResolutionGenerator; public class MarkerHoverControl extends AbstractInformationControl implements IInformationControlExtension2, IInformationControlExtension3, IInformationControlExtension5 { private CompoundRegion region; private Control focusControl; private Composite parent; private final DefaultMarkerAnnotationAccess markerAccess; public MarkerHoverControl(Shell shell, ToolBarManager toolbarManager) { super(shell, toolbarManager); markerAccess = new DefaultMarkerAnnotationAccess(); create(); } public MarkerHoverControl(Shell shell) { super(shell, EditorsUI.getTooltipAffordanceString()); markerAccess = new DefaultMarkerAnnotationAccess(); create(); } /* * @see org.eclipse.jface.text.IInformationControlExtension2#setInput(java.lang.Object) */ public void setInput(Object input) { assert input instanceof CompoundRegion; if(input instanceof CompoundRegion) { region = (CompoundRegion) input; } else { throw new IllegalStateException("Not CompoundRegion"); //$NON-NLS-1$ } disposeDeferredCreatedContent(); deferredCreateContent(); } Shell getMyShell() { return super.getShell(); } Control getRoot() { return parent; } /* * @see org.eclipse.jface.text.IInformationControlExtension#hasContents() */ public boolean hasContents() { return region != null; } /* * @see org.eclipse.jdt.internal.ui.text.java.hover.AbstractAnnotationHover.AbstractInformationControl#setFocus() */ public void setFocus() { super.setFocus(); if(focusControl != null) { focusControl.setFocus(); } } /* * @see org.eclipse.jface.text.AbstractInformationControl#setVisible(boolean) */ public final void setVisible(boolean visible) { if(!visible) disposeDeferredCreatedContent(); super.setVisible(visible); } protected void disposeDeferredCreatedContent() { Control[] children = parent.getChildren(); for(int i = 0; i < children.length; i++ ) { children[i].dispose(); } ToolBarManager toolBarManager = getToolBarManager(); if(toolBarManager != null) toolBarManager.removeAll(); } /* * @see org.eclipse.jface.text.AbstractInformationControl#createContent(org.eclipse.swt.widgets.Composite) */ protected void createContent(Composite parent) { this.parent = parent; GridLayout layout = new GridLayout(1, false); layout.verticalSpacing = 0; layout.marginWidth = 0; layout.marginHeight = 0; parent.setLayout(layout); } @Override public Point computeSizeHint() { Point preferedSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true); Point constrains = getSizeConstraints(); if(constrains == null) return preferedSize; int trimWidth = getShell().computeTrim(0, 0, 0, 0).width; Point constrainedSize = getShell().computeSize(constrains.x - trimWidth, SWT.DEFAULT, true); int width = Math.min(preferedSize.x, constrainedSize.x); int height = Math.max(preferedSize.y, constrainedSize.y); return new Point(width, height); } /** * Create content of the hover. This is called after the input has been set. */ protected void deferredCreateContent() { if(region != null) { final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL); GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); scrolledComposite.setLayoutData(gridData); scrolledComposite.setExpandVertical(true); scrolledComposite.setExpandHorizontal(true); Composite composite = new Composite(scrolledComposite, SWT.NONE); GridLayout layout = new GridLayout(1, false); composite.setLayout(layout); scrolledComposite.setContent(composite); // force resize of scrolledComposite when its content height changes composite.addListener(SWT.Resize, new Listener() { int width = -1; public void handleEvent(Event e) { int newWidth = composite.getSize().x; if(newWidth != width) { scrolledComposite.setMinHeight(composite.computeSize(newWidth, SWT.DEFAULT).y); width = newWidth; } } }); boolean lifecycleMarkers = false; for(IRegion reg : region.getRegions()) { if(reg instanceof PomHyperlinkDetector.MarkerRegion) { PomHyperlinkDetector.MarkerRegion markerReg = (PomHyperlinkDetector.MarkerRegion) reg; IMarker mark = markerReg.getAnnotation().getMarker(); if(MavenDiscoveryMarkerResolutionGenerator.canResolve(mark)) { lifecycleMarkers = true; break; } } } fillToolbar(lifecycleMarkers); for(IRegion reg : region.getRegions()) { if(reg instanceof PomHyperlinkDetector.MarkerRegion) { final PomHyperlinkDetector.MarkerRegion markerReg = (PomHyperlinkDetector.MarkerRegion) reg; createAnnotationInformation(composite, markerReg); final IMarker mark = markerReg.getAnnotation().getMarker(); if(MavenProblemResolution.hasResolutions(mark)) { List<IMarkerResolution> resolutions = MavenProblemResolution.getResolutions(mark); createResolutionsControl(composite, mark, resolutions); } } if(reg instanceof ManagedArtifactRegion) { final ManagedArtifactRegion man = (ManagedArtifactRegion) reg; Composite comp = createTooltipComposite(composite, PomTextHover.getLabelForRegion(man)); //only create the hyperlink when the origin location for jumping is present. //in some cases (managed version comes from imported dependencies) we don't have the location and have nowhere to jump) if(PomHyperlinkDetector.canCreateHyperLink(man)) { Link link = createHyperlink(comp); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { dispose(); PomHyperlinkDetector.createHyperlink(man).open(); } }); } } if(reg instanceof ExpressionRegion) { final ExpressionRegion expr = (ExpressionRegion) reg; Composite tooltipComposite = createTooltipComposite(composite, PomTextHover.getLabelForRegion(expr)); if(PomHyperlinkDetector.canCreateHyperLink(expr)) { Link link = createHyperlink(tooltipComposite); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { dispose(); PomHyperlinkDetector.createHyperlink(expr).open(); } }); } } if(region.getRegions().indexOf(reg) < region.getRegions().size() - 1) { createSeparator(composite); } } Point constraints = getSizeConstraints(); Point contentSize = composite.computeSize(constraints != null ? constraints.x : SWT.DEFAULT, SWT.DEFAULT); composite.setSize(new Point(contentSize.x, contentSize.y)); //12 is the magic number for height of status line } setColorAndFont(parent, parent.getForeground(), parent.getBackground(), JFaceResources.getDialogFont()); parent.layout(true); } protected void fillToolbar(boolean includeLifecycle) { ToolBarManager toolBarManager = getToolBarManager(); if(toolBarManager == null) return; toolBarManager.add(new OpenPreferencesAction(this, // MvnImages.IMGD_WARNINGS, Messages.MarkerHoverControl_openWarningsPrefs, // "org.eclipse.m2e.core.ui.preferences.WarningsPreferencePage")); //$NON-NLS-1$ if(includeLifecycle) { toolBarManager.add(new OpenPreferencesAction(this, // MvnImages.IMGD_EXECUTION, Messages.MarkerHoverControl_openLifecyclePrefs, // "org.eclipse.m2e.core.preferences.LifecycleMappingPreferencePag")); //$NON-NLS-1$ toolBarManager.add(new OpenPreferencesAction(this, // MvnImages.IMGD_DISCOVERY, Messages.MarkerHoverControl_openDiscoveryPrefs, // "org.eclipse.m2e.discovery.internal.preferences.DiscoveryPreferencePage")); //$NON-NLS-1$ } toolBarManager.update(true); } private Link createHyperlink(Composite parent) { Link link = new Link(parent, SWT.NONE); GridData data2 = new GridData(SWT.FILL, SWT.FILL, true, true); data2.horizontalIndent = 18; link.setLayoutData(data2); link.setText(Messages.PomTextHover_jump_to); return link; } private Composite createTooltipComposite(Composite parent, StyledString text) { Composite composite = new Composite(parent, SWT.NONE); composite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); GridLayout layout = new GridLayout(2, false); layout.marginHeight = 2; layout.marginWidth = 2; layout.horizontalSpacing = 0; composite.setLayout(layout); //this paints the icon.. final Canvas canvas = new Canvas(composite, SWT.NO_FOCUS); GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); gridData.widthHint = 17; gridData.heightHint = 16; canvas.setLayoutData(gridData); //and now comes the text StyledText styledtext = new StyledText(composite, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY); GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); styledtext.setLayoutData(data); styledtext.setText(text.getString()); styledtext.setStyleRanges(text.getStyleRanges()); new Label(composite, SWT.NONE); return composite; } private void setColorAndFont(Control control, Color foreground, Color background, Font font) { control.setForeground(foreground); control.setBackground(background); control.setFont(font); if(control instanceof Composite) { Control[] children = ((Composite) control).getChildren(); for(int i = 0; i < children.length; i++ ) { setColorAndFont(children[i], foreground, background, font); } } } private void createAnnotationInformation(Composite parent, final MarkerRegion annotation) { Composite composite = new Composite(parent, SWT.NONE); composite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); GridLayout layout = new GridLayout(2, false); layout.marginHeight = 2; layout.marginWidth = 2; layout.horizontalSpacing = 0; composite.setLayout(layout); //this paints the icon.. final Canvas canvas = new Canvas(composite, SWT.NO_FOCUS); GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); gridData.widthHint = 17; gridData.heightHint = 16; canvas.setLayoutData(gridData); canvas.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { e.gc.setFont(null); markerAccess.paint(annotation.getAnnotation(), e.gc, canvas, new Rectangle(0, 0, 16, 16)); } }); //and now comes the text StyledText text = new StyledText(composite, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY); GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); text.setLayoutData(data); String annotationText = annotation.getAnnotation().getText(); if(annotationText != null) { text.setText(annotationText); } if(annotation.isDefinedInParent()) { new Label(composite, SWT.NONE); Link link = new Link(composite, SWT.NONE); GridData data2 = new GridData(SWT.FILL, SWT.FILL, true, true); data2.horizontalIndent = 18; link.setLayoutData(data2); link.setText(Messages.MarkerHoverControl_openParentDefinition); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { PomHyperlinkDetector.createHyperlink(annotation).open(); dispose(); } }); } } private void createSeparator(Composite parent) { Label separator = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); gridData.verticalIndent = 2; separator.setLayoutData(gridData); } private void createResolutionsControl(Composite parent, IMarker mark, List<IMarkerResolution> resolutions) { Composite composite = new Composite(parent, SWT.NONE); composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); GridLayout layout = new GridLayout(1, false); layout.marginWidth = 0; layout.verticalSpacing = 2; layout.marginHeight = 0; composite.setLayout(layout); Label quickFixLabel = new Label(composite, SWT.NONE); GridData layoutData = new GridData(SWT.BEGINNING, SWT.TOP, false, false); layoutData.horizontalIndent = 4; quickFixLabel.setLayoutData(layoutData); String text; if(resolutions.size() == 1) { text = Messages.PomTextHover_one_quickfix; } else { text = NLS.bind(Messages.PomTextHover_more_quickfixes, String.valueOf(resolutions.size())); } quickFixLabel.setText(text); Composite composite2 = new Composite(parent, SWT.NONE); composite2.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); GridLayout layout2 = new GridLayout(2, false); layout2.marginLeft = 5; layout2.verticalSpacing = 2; composite2.setLayout(layout2); List<Link> list = new ArrayList<>(); for(IMarkerResolution r : resolutions) { list.add(createCompletionProposalLink(composite2, mark, r, 1));// Original link for single fix, hence pass 1 for count } final Link[] links = list.toArray(new Link[list.size()]); focusControl = links.length == 0 ? null : links[0]; for(int i = 0; i < links.length; i++ ) { final int index = i; final Link link = links[index]; link.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { switch(e.keyCode) { case SWT.ARROW_DOWN: if(index + 1 < links.length) { links[index + 1].setFocus(); } break; case SWT.ARROW_UP: if(index > 0) { links[index - 1].setFocus(); } break; default: break; } } public void keyReleased(KeyEvent e) { } }); } } private Link createCompletionProposalLink(Composite parent, final IMarker mark, final IMarkerResolution proposal, int count) { final boolean isMultiFix = count > 1; if(isMultiFix) { new Label(parent, SWT.NONE); // spacer to fill image cell parent = new Composite(parent, SWT.NONE); // indented composite for multi-fix GridLayout layout = new GridLayout(2, false); layout.marginWidth = 0; layout.marginHeight = 0; parent.setLayout(layout); } Label proposalImage = new Label(parent, SWT.NONE); proposalImage.setLayoutData(new GridData(SWT.BEGINNING, SWT.TOP, false, false)); Image image = null; if(proposal instanceof ICompletionProposal) { image = ((ICompletionProposal) proposal).getImage(); } else if(proposal instanceof IMarkerResolution2) { image = ((IMarkerResolution2) proposal).getImage(); } if(image != null) { proposalImage.setImage(image); proposalImage.addMouseListener(new MouseListener() { public void mouseDoubleClick(MouseEvent e) { } public void mouseDown(MouseEvent e) { } public void mouseUp(MouseEvent e) { if(e.button == 1) { apply(proposal, mark, region.textViewer, region.textOffset); } } }); } Link proposalLink = new Link(parent, SWT.WRAP); GridData layoutData = new GridData(SWT.BEGINNING, SWT.TOP, false, false); String linkText; if(isMultiFix) { linkText = NLS.bind(Messages.PomTextHover_category_fix, new Integer(count)); } else { linkText = proposal.getLabel(); } proposalLink.setText("<a>" + linkText + "</a>"); //$NON-NLS-1$ //$NON-NLS-2$ proposalLink.setLayoutData(layoutData); proposalLink.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { apply(proposal, mark, region.textViewer, region.textOffset); } }); return proposalLink; } /** * {@inheritDoc} This default implementation returns <code>null</code>. Subclasses may override. */ public IInformationControlCreator getInformationPresenterControlCreator() { return new IInformationControlCreator() { /* * @see org.eclipse.jface.text.IInformationControlCreator#createInformationControl(org.eclipse.swt.widgets.Shell) */ public IInformationControl createInformationControl(Shell parent) { return new MarkerHoverControl(parent, new ToolBarManager(SWT.FLAT)); } }; } private void apply(IMarkerResolution res, IMarker mark, ITextViewer viewer, int offset) { if(res instanceof ICompletionProposal) { apply((ICompletionProposal) res, viewer, offset, false); } else { dispose(); res.run(mark); } } private void apply(ICompletionProposal p, ITextViewer viewer, int offset, boolean isMultiFix) { //Focus needs to be in the text viewer, otherwise linked mode does not work dispose(); IRewriteTarget target = null; try { IDocument document = viewer.getDocument(); if(viewer instanceof ITextViewerExtension) { ITextViewerExtension extension = (ITextViewerExtension) viewer; target = extension.getRewriteTarget(); } if(target != null) target.beginCompoundChange(); if(p instanceof ICompletionProposalExtension2) { ICompletionProposalExtension2 e = (ICompletionProposalExtension2) p; e.apply(viewer, (char) 0, isMultiFix ? SWT.CONTROL : SWT.NONE, offset); } else if(p instanceof ICompletionProposalExtension) { ICompletionProposalExtension e = (ICompletionProposalExtension) p; e.apply(document, (char) 0, offset); } else { p.apply(document); } Point selection = p.getSelection(document); if(selection != null) { viewer.setSelectedRange(selection.x, selection.y); viewer.revealRange(selection.x, selection.y); } } finally { if(target != null) target.endCompoundChange(); } } private static final class OpenPreferencesAction extends Action { private final IInformationControl infoControl; private String prefsId; public OpenPreferencesAction(IInformationControl infoControl, ImageDescriptor imageDesc, String tooltip, String prefsId) { this.infoControl = infoControl; this.prefsId = prefsId; setImageDescriptor(imageDesc); setToolTipText(tooltip); } @Override public void run() { Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); infoControl.dispose(); PreferencesUtil.createPreferenceDialogOn(shell, prefsId, null, null).open(); } } }