/** * Copyright (c) 2014 by Brainwy Software LTDA. 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.shared_ui.editor; import java.util.Collection; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.JFaceTextUtil; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ExtendedModifyEvent; import org.eclipse.swt.custom.ExtendedModifyListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.StyledTextContent; import org.eclipse.swt.custom.TextChangeListener; import org.eclipse.swt.custom.TextChangedEvent; import org.eclipse.swt.custom.TextChangingEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; 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.Rectangle; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_ui.utils.RunInUiThread; public class VerticalIndentGuidesPainter implements PaintListener, ModifyListener, ExtendedModifyListener, TextChangeListener, DisposeListener { private StyledText styledText; private boolean inDraw; private Rectangle currClientArea; private int currCharCount; private Map<Integer, List<VerticalLinesToDraw>> lineToVerticalLinesToDraw; private StyledTextContent content; private final IVerticalLinesIndentGuideComputer indentGuide; private int lastXOffset = -1; private int lastYOffset = -1; private int currTabWidth = -1; private boolean askFullRedraw = true; //On the first one always make it full /** * Note: dispose doesn't need to be explicitly called (it'll be disposed when * the StyledText set at setStyledText is disposed). Still, calling it more than * once should be ok. */ public void dispose() { styledText = null; currClientArea = null; lineToVerticalLinesToDraw = null; content = null; indentGuide.dispose(); } public VerticalIndentGuidesPainter(IVerticalLinesIndentGuideComputer indentGuide) { Assert.isNotNull(indentGuide); this.indentGuide = indentGuide; } @Override public void paintControl(PaintEvent e) { if (inDraw || styledText == null || styledText.isDisposed()) { return; } try { inDraw = true; boolean showIndentGuide = this.indentGuide.getShowIndentGuide(); if (!showIndentGuide) { return; } int xOffset = styledText.getHorizontalPixel(); int yOffset = styledText.getTopPixel(); //Important: call all to cache the new values (instead of doing all inside the or below). boolean styledTextContentChanged = getStyledTextContentChangedAndStoreNew(); boolean clientAreaChanged = getClientAreaChangedAndStoreNew(); boolean charCountChanged = getCharCountChangedAndStoreNew(); boolean tabWidthChanged = getTabWidthChangedAndStoreNew(); boolean redrawAll = styledTextContentChanged || clientAreaChanged || charCountChanged || tabWidthChanged || xOffset != lastXOffset || yOffset != lastYOffset; StyledTextContent currentContent = this.content; if (currClientArea == null || currClientArea.width < 5 || currClientArea.height < 5 || currCharCount < 1 || currentContent == null || currTabWidth <= 0) { return; } lastXOffset = xOffset; lastYOffset = yOffset; int topIndex; try { topIndex = JFaceTextUtil.getPartialTopIndex(styledText); } catch (IllegalArgumentException e1) { // Just silence it... // java.lang.IllegalArgumentException: Index out of bounds // at org.eclipse.swt.SWT.error(SWT.java:4458) // at org.eclipse.swt.SWT.error(SWT.java:4392) // at org.eclipse.swt.SWT.error(SWT.java:4363) // at org.eclipse.swt.custom.StyledText.getOffsetAtLine(StyledText.java:4405) // at org.eclipse.jface.text.JFaceTextUtil.getPartialTopIndex(JFaceTextUtil.java:103) // at org.python.pydev.shared_ui.editor.VerticalIndentGuidesPainter.paintControl(VerticalIndentGuidesPainter.java:93) return; } int bottomIndex = JFaceTextUtil.getPartialBottomIndex(styledText); if (redrawAll) { this.lineToVerticalLinesToDraw = this.indentGuide.computeVerticalLinesToDrawInRegion(styledText, topIndex, bottomIndex); // This is a bit unfortunate: when something changes, we may have to repaint out of the clipping // region, but even setting the clipping region (e.gc.setClipping), the clipping region may still // be unchanged (because the system said that it only wants to repaint some specific area already // and we can't make it bigger -- so, what's left for us is asking for a repaint of the full area // in this case). if (askFullRedraw) { askFullRedraw = false; if (Math.abs(currClientArea.height - e.gc.getClipping().height) > 40) { //Only do it if the difference is really high (some decorations make it usually a bit lower than //the actual client area -- usually around 14 in my tests, but make it a bit higher as the usual //difference when a redraw is needed is pretty high). RunInUiThread.async(new Runnable() { @Override public void run() { StyledText s = styledText; if (s != null && !s.isDisposed()) { s.redraw(); } } }); } else { } } } if (this.lineToVerticalLinesToDraw != null) { try (AutoCloseable temp = configGC(e.gc)) { Collection<List<VerticalLinesToDraw>> values = lineToVerticalLinesToDraw.values(); for (List<VerticalLinesToDraw> list : values) { for (VerticalLinesToDraw verticalLinesToDraw : list) { verticalLinesToDraw.drawLine(e.gc); } } } } } catch (Exception e1) { Log.log(e1); } finally { inDraw = false; } } private boolean getStyledTextContentChangedAndStoreNew() { StyledTextContent currentContent = this.styledText.getContent(); StyledTextContent oldContent = this.content; if (currentContent != oldContent) { //Important: the content may change during runtime, so, we have to stop listening the old one and //start listening the new one. if (oldContent != null) { oldContent.removeTextChangeListener(this); } this.content = currentContent; currentContent.addTextChangeListener(this); return true; } return false; } private AutoCloseable configGC(final GC gc) { final int lineStyle = gc.getLineStyle(); final int alpha = gc.getAlpha(); final int[] lineDash = gc.getLineDash(); final Color foreground = gc.getForeground(); final Color background = gc.getBackground(); gc.setForeground(this.indentGuide.getColor(styledText)); gc.setBackground(styledText.getBackground()); gc.setAlpha(this.indentGuide.getTransparency()); gc.setLineStyle(SWT.LINE_CUSTOM); gc.setLineDash(new int[] { 1, 2 }); return new AutoCloseable() { @Override public void close() throws Exception { gc.setForeground(foreground); gc.setBackground(background); gc.setAlpha(alpha); gc.setLineStyle(lineStyle); gc.setLineDash(lineDash); } }; } boolean getClientAreaChangedAndStoreNew() { Rectangle clientArea = styledText.getClientArea(); if (currClientArea == null || !currClientArea.equals(clientArea)) { currClientArea = clientArea; return true; } return false; } boolean getCharCountChangedAndStoreNew() { int charCount = styledText.getCharCount(); if (currCharCount != charCount) { currCharCount = charCount; return true; } return false; } boolean getTabWidthChangedAndStoreNew() { int tabWidth = indentGuide.getTabWidth(); if (currTabWidth != tabWidth) { currTabWidth = tabWidth; return true; } return false; } @Override public void widgetDisposed(DisposeEvent e) { this.dispose(); } public void setStyledText(StyledText styledText) { if (this.styledText != null) { this.styledText.removeModifyListener(this); this.styledText.removeExtendedModifyListener(this); if (this.content != null) { this.content.removeTextChangeListener(this); } this.styledText.removeDisposeListener(this); } this.styledText = styledText; this.content = this.styledText.getContent(); this.styledText.addModifyListener(this); this.styledText.addExtendedModifyListener(this); this.content.addTextChangeListener(this); this.styledText.addDisposeListener(this); } @Override public void modifyText(ModifyEvent e) { this.currClientArea = null; //will force redrawing everything askFullRedraw = true; } @Override public void modifyText(ExtendedModifyEvent event) { this.currClientArea = null; //will force redrawing everything askFullRedraw = true; } @Override public void textChanging(TextChangingEvent event) { this.currClientArea = null; //will force redrawing everything askFullRedraw = true; } @Override public void textChanged(TextChangedEvent event) { this.currClientArea = null; //will force redrawing everything askFullRedraw = true; } @Override public void textSet(TextChangedEvent event) { this.currClientArea = null; //will force redrawing everything askFullRedraw = true; } }