/**
* 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();
}
}
}