/******************************************************************************* * Copyright (c) 2000, 2017 IBM Corporation 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 * *******************************************************************************/ package org.eclipse.dltk.internal.ui.text.hover; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jface.text.AbstractInformationControlManager; import org.eclipse.jface.text.DefaultInformationControl; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlExtension; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IViewportListener; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextViewer; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationAccess; import org.eclipse.jface.text.source.IAnnotationAccessExtension; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.IVerticalRulerInfo; import org.eclipse.jface.text.source.IVerticalRulerListener; import org.eclipse.jface.text.source.VerticalRulerEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.MenuEvent; import org.eclipse.swt.events.MenuListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseTrackAdapter; import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Cursor; 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.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Layout; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; /** * A control that can display a number of annotations. The control can decide * how it layouts the annotations to present them to the user. * <p> * This class got moved here form Platform Text since it was not used there and * caused discouraged access warnings. It will be moved down again once * annotation roll-over support is provided by Platform Text. * </p> * <p> * Each annotation can have its custom context menu and hover. * </p> * * @since 3.2 */ public class AnnotationExpansionControl implements IInformationControl, IInformationControlExtension, IInformationControlExtension2 { public interface ICallback { void run(IInformationControlExtension2 control); } /** * Input used by the control to display the annotations. TODO move to * top-level class TODO encapsulate fields * * @since 3.0 */ public static class AnnotationHoverInput { public Annotation[] fAnnotations; public ISourceViewer fViewer; public IVerticalRulerInfo fRulerInfo; public IVerticalRulerListener fAnnotationListener; public IDoubleClickListener fDoubleClickListener; public ICallback redoAction; public IAnnotationModel model; } private final class Item { Annotation fAnnotation; Canvas canvas; StyleRange[] oldStyles; public void selected() { Display disp = fShell.getDisplay(); canvas.setCursor(fHandCursor); // TODO: shade - for now: set grey background canvas.setBackground(getSelectionColor(disp)); // highlight the viewer background at its position oldStyles = setViewerBackground(fAnnotation); // set the selection fSelection = this; if (fHoverManager != null) fHoverManager.showInformation(); if (fInput.fAnnotationListener != null) { VerticalRulerEvent event = new VerticalRulerEvent(fAnnotation); fInput.fAnnotationListener.annotationSelected(event); } } public void defaultSelected() { if (fInput.fAnnotationListener != null) { VerticalRulerEvent event = new VerticalRulerEvent(fAnnotation); fInput.fAnnotationListener.annotationDefaultSelected(event); } dispose(); } public void deselect() { // hide the popup // fHoverManager.disposeInformationControl(); // deselect fSelection = null; resetViewerBackground(oldStyles); oldStyles = null; Display disp = fShell.getDisplay(); canvas.setCursor(null); // TODO: remove shading - for now: set standard background canvas.setBackground( disp.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); } } /** * Disposes of an item */ private final static class MyDisposeListener implements DisposeListener { @Override public void widgetDisposed(DisposeEvent e) { Item item = (Item) ((Widget) e.getSource()).getData(); item.deselect(); item.canvas = null; item.fAnnotation = null; item.oldStyles = null; ((Widget) e.getSource()).setData(null); } } /** * Listener on context menu invocation on the items */ private final class MyMenuDetectListener implements Listener { @Override public void handleEvent(Event event) { if (event.type == SWT.MenuDetect) { // TODO: show per-item menu // for now: show ruler context menu if (fInput != null) { Control ruler = fInput.fRulerInfo.getControl(); if (ruler != null && !ruler.isDisposed()) { Menu menu = ruler.getMenu(); if (menu != null && !menu.isDisposed()) { menu.setLocation(event.x, event.y); menu.addMenuListener(new MenuListener() { @Override public void menuHidden(MenuEvent e) { dispose(); } @Override public void menuShown(MenuEvent e) { } }); menu.setVisible(true); } } } } } } /** * Listener on mouse events on the items. */ private final class MyMouseListener extends MouseAdapter { @Override public void mouseDoubleClick(MouseEvent e) { Item item = (Item) ((Widget) e.getSource()).getData(); if (e.button == 1 && item.fAnnotation == fInput.fAnnotations[0] && fInput.fDoubleClickListener != null) { fInput.fDoubleClickListener.doubleClick(null); // special code for JDT to renew the annotation set. if (fInput.redoAction != null) fInput.redoAction.run(AnnotationExpansionControl.this); } // dispose(); // TODO special action to invoke double-click action on the vertical // ruler // how about // Canvas can= (Canvas) e.getSource(); // Annotation a= (Annotation) can.getData(); // if (a != null) { // a.getDoubleClickAction().run(); // } } @Override public void mouseDown(MouseEvent e) { Item item = (Item) ((Widget) e.getSource()).getData(); // TODO for now, to make double click work: disable single click on // the first item // disable later when the annotationlistener selectively handles // input if (item != null && e.button == 1) // && item.fAnnotation != // fInput.fAnnotations[0]) item.defaultSelected(); } /* * @see * org.eclipse.swt.events.MouseAdapter#mouseDown(org.eclipse.swt.events. * MouseEvent) */ // public void mouseDown(MouseEvent e) { // super.mouseDown(e); // } } /** * Listener on mouse track events on the items. */ private final class MyMouseTrackListener implements MouseTrackListener { @Override public void mouseEnter(MouseEvent e) { Item item = (Item) ((Widget) e.getSource()).getData(); if (item != null) item.selected(); } @Override public void mouseExit(MouseEvent e) { Item item = (Item) ((Widget) e.getSource()).getData(); if (item != null) item.deselect(); // if the event lies outside the entire popup, dispose org.eclipse.swt.graphics.Region region = fShell.getRegion(); Canvas can = (Canvas) e.getSource(); Point p = can.toDisplay(e.x, e.y); if (region == null) { Rectangle bounds = fShell.getBounds(); // p= fShell.toControl(p); if (!bounds.contains(p)) dispose(); } else { p = fShell.toControl(p); if (!region.contains(p)) dispose(); } } @Override public void mouseHover(MouseEvent e) { if (fHoverManager == null) { fHoverManager = new HoverManager(); fHoverManager.takesFocusWhenVisible(false); fHoverManager.install(fComposite); fHoverManager.showInformation(); } } } /** * * * @since 3.0 */ public class LinearLayouter { private static final int ANNOTATION_SIZE = 14; private static final int BORDER_WIDTH = 2; public Layout getLayout(int itemCount) { // simple layout: a row of items GridLayout layout = new GridLayout(itemCount, true); layout.horizontalSpacing = 1; layout.verticalSpacing = 0; layout.marginHeight = 1; layout.marginWidth = 1; return layout; } public Object getLayoutData() { GridData gridData = new GridData(ANNOTATION_SIZE + 2 * BORDER_WIDTH, ANNOTATION_SIZE + 2 * BORDER_WIDTH); gridData.horizontalAlignment = GridData.CENTER; gridData.verticalAlignment = GridData.CENTER; return gridData; } public int getAnnotationSize() { return ANNOTATION_SIZE; } public int getBorderWidth() { return BORDER_WIDTH; } public org.eclipse.swt.graphics.Region getShellRegion(int itemCount) { // no special region - set to null for default shell size return null; } } /** * Listener on paint events on the items. Paints the annotation image on the * given <code>GC</code>. */ private final class MyPaintListener implements PaintListener { @Override public void paintControl(PaintEvent e) { Canvas can = (Canvas) e.getSource(); Annotation a = ((Item) can.getData()).fAnnotation; if (a != null) { Rectangle rect = new Rectangle(fLayouter.getBorderWidth(), fLayouter.getBorderWidth(), fLayouter.getAnnotationSize(), fLayouter.getAnnotationSize()); if (fAnnotationAccessExtension != null) fAnnotationAccessExtension.paint(a, e.gc, can, rect); } } } /** * Our own private hover manager used to shop per-item pop-ups. */ private final class HoverManager extends AbstractInformationControlManager { /** * */ public HoverManager() { super(parent -> new DefaultInformationControl(parent)); setMargins(5, 10); setAnchor(ANCHOR_BOTTOM); setFallbackAnchors( new Anchor[] { ANCHOR_BOTTOM, ANCHOR_LEFT, ANCHOR_RIGHT }); } @Override protected void computeInformation() { if (fSelection != null) { Rectangle subjectArea = fSelection.canvas.getBounds(); Annotation annotation = fSelection.fAnnotation; String msg; if (annotation != null) msg = annotation.getText(); else msg = null; setInformation(msg, subjectArea); } } } /** Model data. */ protected AnnotationHoverInput fInput; /** The control's shell */ private Shell fShell; /** The composite combining all the items. */ protected Composite fComposite; /** The hand cursor. */ private Cursor fHandCursor; /** * The currently selected item, or <code>null</code> if none is selected. */ private Item fSelection; /** The hover manager for the per-item hovers. */ private HoverManager fHoverManager; /** The annotation access extension. */ private IAnnotationAccessExtension fAnnotationAccessExtension; /* listener legion */ private final MyPaintListener fPaintListener; private final MyMouseTrackListener fMouseTrackListener; private final MyMouseListener fMouseListener; private final MyMenuDetectListener fMenuDetectListener; private final DisposeListener fDisposeListener; private final IViewportListener fViewportListener; private LinearLayouter fLayouter; /** * Creates a new control. * * @param parent * @param shellStyle * @param access */ public AnnotationExpansionControl(Shell parent, int shellStyle, IAnnotationAccess access) { fPaintListener = new MyPaintListener(); fMouseTrackListener = new MyMouseTrackListener(); fMouseListener = new MyMouseListener(); fMenuDetectListener = new MyMenuDetectListener(); fDisposeListener = new MyDisposeListener(); fViewportListener = verticalOffset -> dispose(); fLayouter = new LinearLayouter(); if (access instanceof IAnnotationAccessExtension) fAnnotationAccessExtension = (IAnnotationAccessExtension) access; fShell = new Shell(parent, shellStyle | SWT.NO_FOCUS | SWT.ON_TOP); Display display = fShell.getDisplay(); fShell.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); fComposite = new Composite(fShell, SWT.NO_FOCUS | SWT.NO_REDRAW_RESIZE | SWT.NO_TRIM); // fComposite= new Composite(fShell, SWT.NO_FOCUS | SWT.NO_REDRAW_RESIZE // | SWT.NO_TRIM | SWT.V_SCROLL); GridLayout layout = new GridLayout(1, true); layout.marginHeight = 0; layout.marginWidth = 0; fShell.setLayout(layout); GridData data = new GridData(GridData.FILL_BOTH); data.heightHint = fLayouter.getAnnotationSize() + 2 * fLayouter.getBorderWidth() + 4; fComposite.setLayoutData(data); fComposite.addMouseTrackListener(new MouseTrackAdapter() { @Override public void mouseExit(MouseEvent e) { if (fComposite == null) return; Control[] children = fComposite.getChildren(); Rectangle bounds = null; for (int i = 0; i < children.length; i++) { if (bounds == null) bounds = children[i].getBounds(); else bounds.add(children[i].getBounds()); if (bounds.contains(e.x, e.y)) return; } // if none of the children contains the event, we leave the // popup dispose(); } }); // fComposite.getVerticalBar().addListener(SWT.Selection, new Listener() // { // // public void handleEvent(Event event) { // Rectangle bounds= fShell.getBounds(); // int x= bounds.x - fLayouter.getAnnotationSize() - // fLayouter.getBorderWidth(); // int y= bounds.y; // fShell.setBounds(x, y, bounds.width, bounds.height); // } // // }); fHandCursor = new Cursor(display, SWT.CURSOR_HAND); fShell.setCursor(fHandCursor); fComposite.setCursor(fHandCursor); setInfoSystemColor(); } private void setInfoSystemColor() { Display display = fShell.getDisplay(); setForegroundColor(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); setBackgroundColor(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); } @Override public void setInformation(String information) { setInput(null); } @Override public void setInput(Object input) { if (fInput != null && fInput.fViewer != null) fInput.fViewer.removeViewportListener(fViewportListener); if (input instanceof AnnotationHoverInput) fInput = (AnnotationHoverInput) input; else fInput = null; inputChanged(fInput, null); } protected void inputChanged(Object newInput, Object newSelection) { refresh(); } protected void refresh() { adjustItemNumber(); if (fInput == null) return; if (fInput.fAnnotations == null) return; if (fInput.fViewer != null) fInput.fViewer.addViewportListener(fViewportListener); fShell.setRegion(fLayouter.getShellRegion(fInput.fAnnotations.length)); Layout layout = fLayouter.getLayout(fInput.fAnnotations.length); fComposite.setLayout(layout); Control[] children = fComposite.getChildren(); for (int i = 0; i < fInput.fAnnotations.length; i++) { Canvas canvas = (Canvas) children[i]; Item item = new Item(); item.canvas = canvas; item.fAnnotation = fInput.fAnnotations[i]; canvas.setData(item); canvas.redraw(); } } protected void adjustItemNumber() { if (fComposite == null) return; Control[] children = fComposite.getChildren(); int oldSize = children.length; int newSize = fInput == null ? 0 : fInput.fAnnotations.length; Display display = fShell.getDisplay(); // add missing items for (int i = oldSize; i < newSize; i++) { Canvas canvas = new Canvas(fComposite, SWT.NONE); Object gridData = fLayouter.getLayoutData(); canvas.setLayoutData(gridData); canvas.setBackground( display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); canvas.addPaintListener(fPaintListener); canvas.addMouseTrackListener(fMouseTrackListener); canvas.addMouseListener(fMouseListener); canvas.addListener(SWT.MenuDetect, fMenuDetectListener); canvas.addDisposeListener(fDisposeListener); } // dispose of exceeding resources for (int i = oldSize; i > newSize; i--) { Item item = (Item) children[i - 1].getData(); item.deselect(); children[i - 1].dispose(); } } @Override public void setVisible(boolean visible) { fShell.setVisible(visible); } @Override public void dispose() { if (fShell != null) { if (!fShell.isDisposed()) fShell.dispose(); fShell = null; fComposite = null; if (fHandCursor != null) fHandCursor.dispose(); fHandCursor = null; if (fHoverManager != null) fHoverManager.dispose(); fHoverManager = null; fSelection = null; } } @Override public boolean hasContents() { return fInput.fAnnotations != null && fInput.fAnnotations.length > 0; } @Override public void setSizeConstraints(int maxWidth, int maxHeight) { // fMaxWidth= maxWidth; // fMaxHeight= maxHeight; } @Override public Point computeSizeHint() { return fShell.computeSize(SWT.DEFAULT, SWT.DEFAULT); } @Override public void setLocation(Point location) { fShell.setLocation(location); } @Override public void setSize(int width, int height) { fShell.setSize(width, height); } @Override public void addDisposeListener(DisposeListener listener) { fShell.addDisposeListener(listener); } @Override public void removeDisposeListener(DisposeListener listener) { fShell.removeDisposeListener(listener); } @Override public void setForegroundColor(Color foreground) { fComposite.setForeground(foreground); } @Override public void setBackgroundColor(Color background) { fComposite.setBackground(background); } @Override public boolean isFocusControl() { if (fComposite.isFocusControl()) return true; Control[] children = fComposite.getChildren(); for (int i = 0; i < children.length; i++) { if (children[i].isFocusControl()) return true; } return false; } @Override public void setFocus() { fShell.forceFocus(); } @Override public void addFocusListener(FocusListener listener) { fShell.addFocusListener(listener); } @Override public void removeFocusListener(FocusListener listener) { fShell.removeFocusListener(listener); } private StyleRange[] setViewerBackground(Annotation annotation) { StyledText text = fInput.fViewer.getTextWidget(); if (text == null || text.isDisposed()) return null; Display disp = text.getDisplay(); Position pos = fInput.model.getPosition(annotation); if (pos == null) return null; IRegion region = ((TextViewer) fInput.fViewer) .modelRange2WidgetRange(new Region(pos.offset, pos.length)); if (region == null) return null; StyleRange[] ranges = text.getStyleRanges(region.getOffset(), region.getLength()); List<StyleRange> undoRanges = new ArrayList<>(ranges.length); for (int i = 0; i < ranges.length; i++) { undoRanges.add((StyleRange) ranges[i].clone()); } int offset = region.getOffset(); StyleRange current = undoRanges.size() > 0 ? (StyleRange) undoRanges.get(0) : null; int curStart = current != null ? current.start : region.getOffset() + region.getLength(); int curEnd = current != null ? current.start + current.length : -1; int index = 0; // fill no-style regions while (curEnd < region.getOffset() + region.getLength()) { // add empty range if (curStart > offset) { StyleRange undoRange = new StyleRange(offset, curStart - offset, null, null); undoRanges.add(index, undoRange); index++; } // step index++; if (index < undoRanges.size()) { offset = curEnd; current = undoRanges.get(index); curStart = current.start; curEnd = current.start + current.length; } else if (index == undoRanges.size()) { // last one offset = curEnd; current = null; curStart = region.getOffset() + region.getLength(); curEnd = -1; } else curEnd = region.getOffset() + region.getLength(); } // create modified styles (with background) List shadedRanges = new ArrayList(undoRanges.size()); for (Iterator it = undoRanges.iterator(); it.hasNext();) { StyleRange range = (StyleRange) ((StyleRange) it.next()).clone(); shadedRanges.add(range); range.background = getHighlightColor(disp); } // set the ranges one by one for (Iterator iter = shadedRanges.iterator(); iter.hasNext();) { text.setStyleRange((StyleRange) iter.next()); } return undoRanges.toArray(undoRanges.toArray(new StyleRange[0])); } private void resetViewerBackground(StyleRange[] oldRanges) { if (oldRanges == null) return; if (fInput == null) return; StyledText text = fInput.fViewer.getTextWidget(); if (text == null || text.isDisposed()) return; // set the ranges one by one for (int i = 0; i < oldRanges.length; i++) { text.setStyleRange(oldRanges[i]); } } private Color getHighlightColor(Display disp) { return disp.getSystemColor(SWT.COLOR_GRAY); } private Color getSelectionColor(Display disp) { return disp.getSystemColor(SWT.COLOR_GRAY); } }