/** * Copyright (c) 2013-2014 by Brainwy Software Ltda, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.overview_ruler; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.resource.StringConverter; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.IAnnotationAccess; import org.eclipse.jface.text.source.ISharedTextColors; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Transform; 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.ui.editors.text.EditorsUI; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.python.pydev.shared_core.callbacks.ICallbackListener; import org.python.pydev.shared_core.callbacks.ICallbackWithListeners; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.structure.FastStack; import org.python.pydev.shared_ui.SharedUiPlugin; import org.python.pydev.shared_ui.outline.IOutlineModel; import org.python.pydev.shared_ui.outline.IParsedItem; import org.python.pydev.shared_ui.utils.RunInUiThread; public class MinimapOverviewRuler extends CopiedOverviewRuler { private Color selectionColor; private IPropertyChangeListener listener; private IPreferenceStore preferenceStore; private IOutlineModel fOutlineModel; private IPropertyChangeListener propertyListener; private ICallbackListener<IOutlineModel> modelListener; private Color getSelectionColor() { if (selectionColor == null || selectionColor.isDisposed()) { preferenceStore = SharedUiPlugin.getDefault().getPreferenceStore(); fillSelectionColorField(); this.listener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { if (MinimapOverviewRulerPreferencesPage.MINIMAP_SELECTION_COLOR.equals(event.getProperty())) { selectionColor.dispose(); selectionColor = null; fillSelectionColorField(); } } }; preferenceStore.addPropertyChangeListener(listener); } return selectionColor; } private void fillSelectionColorField() { String colorCode = preferenceStore.getString(MinimapOverviewRulerPreferencesPage.MINIMAP_SELECTION_COLOR); RGB asRGB = StringConverter.asRGB(colorCode); selectionColor = new Color(Display.getDefault(), asRGB); } @Override protected void handleDispose() { try { if (preferenceStore != null && listener != null) { preferenceStore.removePropertyChangeListener(listener); preferenceStore = null; listener = null; } } catch (Exception e) { Log.log(e); } try { if (preferenceStore != null && propertyListener != null) { preferenceStore.removePropertyChangeListener(propertyListener); preferenceStore = null; listener = null; } } catch (Exception e) { Log.log(e); } try { if (selectionColor != null) { selectionColor.dispose(); } selectionColor = null; } catch (Exception e) { Log.log(e); } try { if (fOutlineModel != null && modelListener != null) { ICallbackWithListeners<IOutlineModel> onModelChangedListener = fOutlineModel .getOnModelChangedCallback(); onModelChangedListener.unregisterListener(modelListener); modelListener = null; } } catch (Exception e) { Log.log(e); } fOutlineModel = null; super.handleDispose(); } /** * Removes whitespaces and tabs at the end of the string. */ public static String rightTrim(final String input) { int len = input.length(); int st = 0; int off = 0; while ((st < len) && (input.charAt(off + len - 1) <= ' ')) { len--; } return input.substring(0, len); } /** * Helper to get the first char position in a string. */ public static int getFirstCharPosition(String src) { int i = 0; boolean breaked = false; while (i < src.length()) { if (Character.isWhitespace(src.charAt(i)) == false && src.charAt(i) != '\t') { i++; breaked = true; break; } i++; } if (!breaked) { i++; } return (i - 1); } /** * Lock to access the stacked parameters. */ private final static Object lockStackedParameters = new Object(); @SuppressWarnings("unused") private static final class Parameters { public final GC gc; public final Color styledTextForeground; public final Point size; public final int lineCount; public final int marginCols; public final Color marginColor; public final int spacing; public final int imageHeight; public final Transform transform; public final Image tmpImage; public Parameters(GC gc, Color styledTextForeground, Point size, int lineCount, int marginCols, Color marginColor, int spacing, int imageHeight, Transform transform, Image tmpImage) { this.gc = gc; this.styledTextForeground = styledTextForeground; this.size = size; this.lineCount = lineCount; this.marginCols = marginCols; this.marginColor = marginColor; this.spacing = spacing; this.imageHeight = imageHeight; this.transform = transform; this.tmpImage = tmpImage; } public void dispose() { gc.dispose(); marginColor.dispose(); transform.dispose(); } public boolean isDisposed() { if (gc.isDisposed()) { return true; } if (marginColor.isDisposed()) { return true; } if (tmpImage.isDisposed()) { return true; } return false; } } /** * Redraws a temporary image in the background and after that's finished, replaces the new base image and asks * for a new redraw. */ private final class RedrawJob extends Job { private RedrawJob(String name) { super(name); this.setPriority(Job.SHORT); this.setSystem(true); } private FastStack<Parameters> stackedParameters = new FastStack<Parameters>(20); /** * Note: the GC and marginColor need to be disposed after they're used. */ private void setParameters(Parameters parameters) { synchronized (lockStackedParameters) { stackedParameters.push(parameters); } } /** * Redraws the base image based on the StyledText contents. * * (i.e.: draw the lines) */ private void redrawBaseImage(Parameters parameters, IProgressMonitor monitor) { if (MinimapOverviewRulerPreferencesPage.getShowMinimapContents() && parameters.lineCount > 0 && parameters.size.x > 0) { GC gc = parameters.gc; gc.setForeground(parameters.styledTextForeground); gc.setAlpha(200); gc.setTransform(parameters.transform); IOutlineModel outlineModel = fOutlineModel; int x1, x2, y, beginLine; if (outlineModel != null) { IParsedItem root = outlineModel.getRoot(); if (root == null) { Log.log("Minimap overview ruler is trying to use outlineModel which was already disposed."); return; } IParsedItem[] children = root.getChildren(); for (IParsedItem iParsedItem : children) { if (monitor.isCanceled()) { return; } beginLine = iParsedItem.getBeginLine() - 1; y = (int) ((float) beginLine * parameters.imageHeight / parameters.lineCount); x1 = iParsedItem.getBeginCol(); x2 = x1 + (iParsedItem.toString().length() * 5); gc.drawLine(x1, y, x2 - x1, y); IParsedItem[] children2 = iParsedItem.getChildren(); for (IParsedItem iParsedItem2 : children2) { if (monitor.isCanceled()) { return; } beginLine = iParsedItem2.getBeginLine() - 1; y = (int) ((float) beginLine * parameters.imageHeight / parameters.lineCount); x1 = iParsedItem2.getBeginCol(); x2 = x1 + (iParsedItem2.toString().length() * 5); gc.drawLine(x1, y, x2 - x1, y); } } } //This would draw the margin. //gc.setForeground(marginColor); //gc.setBackground(marginColor); //gc.drawLine(marginCols, 0, marginCols, imageHeight); } } /** * Calls the method to draw image and later replaces the base image to be used and calls a new redraw. */ @Override protected IStatus run(IProgressMonitor monitor) { final Parameters parameters; List<Parameters> stackedParametersClone; synchronized (lockStackedParameters) { if (stackedParameters.empty()) { //Not much to do in this case... return Status.OK_STATUS; } parameters = stackedParameters.pop(); stackedParametersClone = fetchStackedParameters(); } disposeStackedParameters(stackedParametersClone); if (parameters.isDisposed()) { return Status.OK_STATUS; } try { redrawBaseImage(parameters, monitor); } catch (Throwable e) { Log.log(e); } finally { parameters.gc.dispose(); parameters.marginColor.dispose(); } boolean disposeOfImage = true; try { if (!monitor.isCanceled()) { final Canvas c = fCanvas; if (c != null && !c.isDisposed()) { disposeOfImage = false; RunInUiThread.async(new Runnable() { @Override public void run() { //The baseImage should only be disposed in the UI thread (so, no locks are needed to //replace/dispose the image) if (baseImage != null && !baseImage.isDisposed()) { baseImage.dispose(); } if (c != null && !c.isDisposed()) { baseImage = parameters.tmpImage; MinimapOverviewRuler.this.redraw(); } else { parameters.tmpImage.dispose(); } } }); } } } finally { if (disposeOfImage) { parameters.tmpImage.dispose(); } } return Status.OK_STATUS; } private List<Parameters> fetchStackedParameters() { ArrayList<Parameters> stackedParametersClone = new ArrayList<Parameters>(); synchronized (lockStackedParameters) { while (stackedParameters.size() > 0) { Parameters disposeOfParameters = stackedParameters.pop(); stackedParametersClone.add(disposeOfParameters); } } return stackedParametersClone; } /** * Disposes of any parameters in the stack that need an explicit dispose(). */ public void disposeStackedParameters() { disposeStackedParameters(fetchStackedParameters()); } private void disposeStackedParameters(List<Parameters> stackedParametersClone) { for (Parameters disposeOfParameters : stackedParametersClone) { disposeOfParameters.dispose(); } } } public MinimapOverviewRuler(IAnnotationAccess annotationAccess, ISharedTextColors sharedColors, IOutlineModel outlineModel) { super(annotationAccess, MinimapOverviewRulerPreferencesPage.getMinimapWidth(), sharedColors); this.fOutlineModel = outlineModel; propertyListener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { if (MinimapOverviewRulerPreferencesPage.MINIMAP_WIDTH.equals(event.getProperty())) { updateWidth(); } } }; if (outlineModel != null) { modelListener = new ICallbackListener<IOutlineModel>() { @Override public Object call(IOutlineModel obj) { lastModelChange = System.currentTimeMillis(); update(); return null; } }; ICallbackWithListeners<IOutlineModel> onModelChangedListener = outlineModel.getOnModelChangedCallback(); onModelChangedListener.registerListener(modelListener); } } private void updateWidth() { fWidth = MinimapOverviewRulerPreferencesPage.getMinimapWidth(); } private WeakReference<StyledText> styledText; private final PaintListener paintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { if (!fCanvas.isDisposed()) { MinimapOverviewRuler.this.redraw(); } } }; private Color lastBackground; private Color lastForeground; @Override protected void doubleBufferPaint(GC dest) { if (fTextViewer != null) { StyledText textWidget = fTextViewer.getTextWidget(); //Calling setBackground/setForeground leads to a repaint on some Linux variants (ubuntu 12), so //we must only call it if it actually changed to prevent a repaint. //View: https://sw-brainwy.rhcloud.com/tracker/LiClipse/120 Color background = textWidget.getBackground(); if (lastBackground == null || !lastBackground.equals(background)) { fCanvas.setBackground(background); lastBackground = background; } Color foreground = textWidget.getForeground(); if (lastForeground == null || !lastForeground.equals(foreground)) { fCanvas.setForeground(foreground); lastForeground = foreground; } } super.doubleBufferPaint(dest); } @Override public Control createControl(Composite parent, ITextViewer textViewer) { Control ret = super.createControl(parent, textViewer); fCanvas.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent event) { onMouseMove(event); } }); fCanvas.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent event) { onDispose(); } }); StyledText textWidget = textViewer.getTextWidget(); if (!textWidget.isDisposed()) { styledText = new WeakReference<StyledText>(textWidget); textWidget.addPaintListener(paintListener); } return ret; } private void onMouseMove(MouseEvent event) { if ((event.stateMask & SWT.BUTTON1) != 0) { handleDrag(event); } } private void onDispose() { try { if (baseImage != null && !baseImage.isDisposed()) { baseImage.dispose(); baseImage = null; } if (lastImage != null && !lastImage.isDisposed()) { lastImage.dispose(); lastImage = null; } if (styledText != null) { StyledText textWidget = styledText.get(); if (textWidget != null && !textWidget.isDisposed()) { textWidget.removePaintListener(paintListener); } } } catch (Throwable e) { Log.log(e); } try { redrawJob.cancel(); redrawJob.disposeStackedParameters(); } catch (Throwable e) { Log.log(e); } } private volatile Image baseImage; private volatile Image lastImage; private Object[] cacheKey; private long lastModelChange; private final RedrawJob redrawJob = new RedrawJob("Redraw overview ruler"); @Override protected void doPaint1(GC paintGc) { //Draw the minimap if (fTextViewer != null) { IDocumentExtension4 document = (IDocumentExtension4) fTextViewer.getDocument(); if (document != null) { final StyledText styledText = fTextViewer.getTextWidget(); final Point size = fCanvas.getSize(); if (size.x != 0 && size.y != 0) { final int lineCount = super.getLineCount(styledText); IPreferenceStore preferenceStore = EditorsUI.getPreferenceStore(); final int marginCols = preferenceStore .getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN); String strColor = preferenceStore .getString(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLOR); RGB marginRgb = StringConverter.asRGB(strColor); Color marginColor = new Color(Display.getCurrent(), marginRgb); int maxChars = (int) (marginCols + (marginCols * 0.1)); final int spacing = 1; int imageHeight = lineCount * spacing; int imageWidth = maxChars; Color background = styledText.getBackground(); boolean isDark = (background.getRed() * 0.21) + (background.getGreen() * 0.71) + (background.getBlue() * 0.07) <= 128; Object[] currCacheKey = new Object[] { document.getModificationStamp(), size.x, size.y, styledText.getForeground(), background, marginCols, marginRgb, lastModelChange }; double scaleX = size.x / (double) imageWidth; double scaleY = size.y / (double) imageHeight; Transform transform = new Transform(Display.getCurrent()); transform.scale((float) scaleX, (float) scaleY); final Color styledTextForeground = styledText.getForeground(); if (baseImage == null || !Arrays.equals(this.cacheKey, currCacheKey)) { this.cacheKey = currCacheKey; Image tmpImage = new Image(Display.getCurrent(), size.x, size.y); final GC gc = new GC(tmpImage); gc.setAdvanced(true); gc.setAntialias(SWT.ON); gc.setBackground(background); gc.setForeground(background); gc.fillRectangle(0, 0, size.x, size.y); final Color marginColor2 = new Color(Display.getCurrent(), marginRgb); redrawJob.cancel(); redrawJob.setParameters(new Parameters(gc, styledTextForeground, size, lineCount, marginCols, marginColor2, spacing, imageHeight, transform, tmpImage)); redrawJob.schedule(); } try { if (baseImage != null && !baseImage.isDisposed()) { if (lastImage != null && !lastImage.isDisposed()) { lastImage.dispose(); } Image image = new Image(Display.getCurrent(), size.x, size.y); GC gc2 = new GC(image); gc2.setAntialias(SWT.ON); try { gc2.setBackground(background); gc2.fillRectangle(0, 0, size.x, size.y); gc2.drawImage(baseImage, 0, 0); Rectangle clientArea = styledText.getClientArea(); int top = styledText.getLineIndex(0); int bottom = styledText.getLineIndex(clientArea.height) + 1; float rect[] = new float[] { 0, top * spacing, imageWidth, (bottom * spacing) - (top * spacing) }; transform.transform(rect); //Draw only a line at the left side. gc2.setLineWidth(3); gc2.setAlpha(30); gc2.setForeground(styledTextForeground); gc2.drawLine(0, 0, 0, size.y); //Draw the selection area if (!isDark) { gc2.setAlpha(30); } else { gc2.setAlpha(100); } Color localSelectionColor = this.getSelectionColor(); if (localSelectionColor.isDisposed()) { //Shouldn't really happen as we should do all in the main thread, but just in case... localSelectionColor = styledText.getSelectionBackground(); } gc2.setForeground(localSelectionColor); gc2.setBackground(localSelectionColor); //Fill selected area in the overview ruler. gc2.fillRectangle(Math.round(rect[0]), Math.round(rect[1]), Math.round(rect[2]), Math.round(rect[3])); //Draw a border around the selected area gc2.setAlpha(255); gc2.setLineWidth(1); gc2.drawRectangle(Math.round(rect[0]), Math.round(rect[1]), Math.round(rect[2]) - 1, Math.round(rect[3])); //This would draw a border around the whole overview bar. //gc2.drawRectangle(0, 0, size.x, size.y); } finally { gc2.dispose(); } lastImage = image; } if (lastImage != null && !lastImage.isDisposed()) { paintGc.drawImage(lastImage, 0, 0); } } finally { marginColor.dispose(); } } } } super.doPaint1(paintGc); } MouseEvent lastMouseDown = null; @Override protected void handleMouseDown(MouseEvent event) { this.handleDrag(event); lastMouseDown = event; } @Override protected void handleMouseUp(MouseEvent event) { if (lastMouseDown != null) { int diff = Math.abs(lastMouseDown.x - event.x); if (diff > 3) { return; } diff = Math.abs(lastMouseDown.y - event.y); if (diff > 3) { return; } if (lastMouseDown.time - event.time < 1000) { super.handleMouseDown(event); } } lastMouseDown = null; } /** * Handles mouse clicks. * * @param event the mouse button down event */ private void handleDrag(MouseEvent event) { if (fTextViewer != null) { int[] lines = toLineNumbers(event.y); int selectedLine = lines[0]; Position p = null; try { IDocument document = fTextViewer.getDocument(); IRegion lineInformation = document.getLineInformation(selectedLine); p = new Position(lineInformation.getOffset(), 0); if (p != null) { StyledText styledText = fTextViewer.getTextWidget(); Rectangle clientArea = styledText.getClientArea(); int top = styledText.getLineIndex(0); int bottom = styledText.getLineIndex(clientArea.height) + 1; int middle = (int) (((bottom - top) / 2.0)); if (selectedLine < middle) { fTextViewer.setTopIndex(0); } else { fTextViewer.setTopIndex(selectedLine - middle); } } } catch (BadLocationException e) { // do nothing } fTextViewer.getTextWidget().setFocus(); } } }