/******************************************************************************* * Copyright (c) 2017 Fabio Zadrozny 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: * Fabio Zadrozny <fabiofz@gmail.com> - initial API and implementation *******************************************************************************/ package org.eclipse.e4.ui.internal.css.swt.dom.scrollbar; import org.eclipse.core.runtime.Platform; import org.eclipse.e4.ui.internal.css.swt.CSSActivator; import org.eclipse.swt.SWT; 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.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Scrollable; import org.osgi.service.log.LogService; /** * An implementation which covers showing themed scrollbars for a StyledText. */ public class StyledTextThemedScrollBarAdapter extends AbstractThemedScrollBarAdapter { private final int fInitialRightMargin; private final int fInitialBottomMargin; private final static boolean isWindowsOS = Platform.OS_WIN32.equals(Platform.getOS()); public StyledTextThemedScrollBarAdapter(StyledText styledText) { this(styledText, new ScrollBarSettings()); } private StyledTextThemedScrollBarAdapter(StyledText styledText, IScrollBarSettings scrollBarSettings) { super(styledText, new StyledTextHorizontalScrollHandler(styledText, scrollBarSettings), new StyledTextVerticalScrollHandler(styledText, scrollBarSettings), scrollBarSettings); fInitialRightMargin = styledText.getRightMargin(); fInitialBottomMargin = styledText.getBottomMargin(); } @Override protected IScrollBarPainter createPaintListener() { return new StyledTextPaintListener(fHorizontalScrollHandler, fVerticalScrollHandler, fScrollBarSettings, fInitialBottomMargin, fInitialRightMargin); } @Override protected Point computeHorizontalAndTopPixel() { StyledText styledText = (StyledText) fScrollable; return new Point(styledText.getHorizontalPixel(), styledText.getTopPixel()); } /** * Ideally this whole class wouldn't be needed (i.e.: if we knew when the * scroll max/selection changed), but unfortunately, these notifications * aren't reliable, so, this class is used to poll such a change when the * text on the StyledText changes. */ static abstract class AbstractStyledTextScrollHandler extends AbstractScrollHandler implements ModifyListener, TextChangeListener { private final StyledText fStyledText; private AbstractThemedScrollBarAdapter fAbstractThemedScrollBarAdapter; private StyledTextContent fTextContent; private int fLastMax; private int fLastSelection; private int fCheckedTimes; protected AbstractStyledTextScrollHandler(StyledText styledText, ScrollBar scrollBar, IScrollBarSettings scrollBarSettings) { super(scrollBar, scrollBarSettings); this.fStyledText = styledText; this.fStyledText.setAlwaysShowScrollBars(true); } @Override protected void checkScrollbarInvisible() { if (this.fScrollBar == null || this.fScrollBar.isDisposed() || !this.fScrollBarSettings.getScrollBarThemed()) { return; } if (this.fScrollBar.isVisible()) { if (fCheckedTimes > 20) { // If some client continually tries to make it visible, // we'll skip trying to put it off... Note that this // will only be fixed when SWT provides an API which // allows us to actually override the scrollbar (so that // visibility changes affect the themed scrollbar and // not the native one). return; } fCheckedTimes++; Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (fStyledText.isDisposed()) { return; } if (!fStyledText.getAlwaysShowScrollBars()) { // We conflict with the setting to make it // visible or invisible fStyledText.setAlwaysShowScrollBars(true); } if (fScrollBar != null && !fScrollBar.isDisposed()) { fScrollBar.setVisible(false); } } }); } } @Override public void install(AbstractThemedScrollBarAdapter abstractThemedScrollBarAdapter) { super.install(abstractThemedScrollBarAdapter); fStyledText.addModifyListener(this); this.fAbstractThemedScrollBarAdapter = abstractThemedScrollBarAdapter; fTextContent = fStyledText.getContent(); fTextContent.addTextChangeListener(this); if(fScrollBar != null){ fLastMax = fScrollBar.getMaximum(); fLastSelection = fScrollBar.getSelection(); } } @Override public void uninstall(AbstractThemedScrollBarAdapter abstractThemedScrollBarAdapter, boolean disposing) { super.uninstall(abstractThemedScrollBarAdapter, disposing); fStyledText.removeModifyListener(this); if (fTextContent != null) { fTextContent.removeTextChangeListener(this); fTextContent = null; } this.fAbstractThemedScrollBarAdapter = null; } private void checkNeedUpdate() { if(fScrollBar != null){ if (fLastMax != fScrollBar.getMaximum() || fLastSelection != fScrollBar.getSelection()) { this.fAbstractThemedScrollBarAdapter.fPainter.redrawScrollBars(); } } } @Override public void modifyText(ModifyEvent e) { checkNeedUpdate(); } @Override public void textSet(TextChangedEvent event) { checkNeedUpdate(); } @Override public void textChanged(TextChangedEvent event) { checkNeedUpdate(); } @Override public void textChanging(TextChangingEvent event) { } @Override public void paintControl(GC gc, Rectangle currClientArea, Scrollable scrollable) { // At each paint, check if the content changed and keep the last // max/selection (unfortunately, it doesn't provide a reliable way // to listen such changes, so, we must poll it). if(fScrollBar != null){ fLastMax = fScrollBar.getMaximum(); fLastSelection = fScrollBar.getSelection(); } if (fTextContent != null && fStyledText.getContent() != fTextContent) { fTextContent.removeTextChangeListener(this); fTextContent = fStyledText.getContent(); fTextContent.addTextChangeListener(this); } super.paintControl(gc, currClientArea, scrollable); } } /** * See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=514068 * * This function will force a synchronous drawing on the styled text on * platforms where just calling a redraw may take some time to actually take * effect (i.e.: windows). See: https://bugs.eclipse.org/511596#c26 for * another similar issue, which makes the drawing of the contents of the * editor appear unsynchronized with the ruler. * * @param styledText * the control where the sync is needed. */ private static void syncStyledTextDrawing(StyledText styledText) { if (isWindowsOS) { styledText.update(); } } /** * Handles the scroll vertically. */ static class StyledTextVerticalScrollHandler extends AbstractStyledTextScrollHandler { public StyledTextVerticalScrollHandler(StyledText styledText, IScrollBarSettings scrollBarSettings) { super(styledText, styledText.getVerticalBar(), scrollBarSettings); } @Override public void setPixel(Scrollable scrollable, int pixel) { StyledText styledText = (StyledText) scrollable; styledText.setTopPixel(pixel); } @Override protected Rectangle getFullBackgroundRect(Scrollable scrollable, Rectangle currClientArea, boolean considerMargins) { StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height; if (considerMargins) { h -= (styledText.getTopMargin() + styledText.getBottomMargin()); } Rectangle rect = new Rectangle(w - lineWidth, considerMargins ? styledText.getTopMargin() : 0, lineWidth, h); return rect; } @Override public Rectangle computeProximityRect(Rectangle currClientArea) { if (this.fScrollBar == null || !this.getVisible()) { return null; } int lineWidth = getMouseNearScrollScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height; Rectangle rect = new Rectangle(w - lineWidth, 0, lineWidth, h); rect.width += 30; rect.x -= 15; return rect; } @Override protected int getRelevantPositionFromPos(Point styledTextPos) { return styledTextPos.y; } @Override public boolean computePositions(Rectangle currClientArea, Scrollable scrollable) { fHandleDrawnRect = null; if (this.fScrollBar == null || this.fScrollBar.getMaximum() - this.fScrollBar.getMinimum() <= 1 || !getVisible() || !this.fScrollBarSettings.getScrollBarThemed()) { return false; } StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height - (styledText.getTopMargin() + styledText.getBottomMargin()); this.fScrollBarPositions = new ScrollBarPositions.ScrollBarPositionsVertical(this.fScrollBar.getMinimum(), this.fScrollBar.getMaximum(), styledText.getTopPixel(), h, w); fHandleDrawnRect = fScrollBarPositions.getHandleDrawRect(lineWidth); if (fHandleDrawnRect == null || h <= fHandleDrawnRect.height) { return false; } return true; } @Override public void doPaintControl(GC gc, Rectangle currClientArea, Scrollable scrollable) { if (fHandleDrawnRect != null) { StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height - (styledText.getTopMargin() + styledText.getBottomMargin()); int borderRadius = Math.min(fScrollBarSettings.getScrollBarBorderRadius(), lineWidth); // Fill the background (same thing as the // getFullBackgroundRect). gc.fillRoundRectangle(w - lineWidth, styledText.getTopMargin(), lineWidth, h, borderRadius, borderRadius); // Fill the foreground. Color foreground = gc.getForeground(); Color background = gc.getBackground(); gc.setBackground(foreground); gc.fillRoundRectangle(fHandleDrawnRect.x, fHandleDrawnRect.y, fHandleDrawnRect.width, fHandleDrawnRect.height, borderRadius, borderRadius); gc.setBackground(background); } } /** * This function is overridden to force an instantaneous redraw in the * StyledText whenever we change the selection on the vertical * scrollbar. */ @Override protected void notifyScrollbarSelectionChanged(Scrollable scrollable, int detail) { super.notifyScrollbarSelectionChanged(scrollable, detail); StyledText styledText = (StyledText) scrollable; syncStyledTextDrawing(styledText); } } /** * Handles the scroll horizontally. */ /* default */ static class StyledTextHorizontalScrollHandler extends AbstractStyledTextScrollHandler { public StyledTextHorizontalScrollHandler(StyledText styledText, IScrollBarSettings scrollBarSettings) { super(styledText, styledText.getHorizontalBar(), scrollBarSettings); } @Override public void setPixel(Scrollable scrollable, int pixel) { StyledText styledText = (StyledText) scrollable; styledText.setHorizontalPixel(pixel); } @Override protected Rectangle getFullBackgroundRect(Scrollable scrollable, Rectangle currClientArea, boolean considerMargins) { StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height; if (considerMargins) { w -= (styledText.getLeftMargin() + styledText.getRightMargin()); } Rectangle rect = new Rectangle(considerMargins ? styledText.getLeftMargin() : 0, h - lineWidth, w, lineWidth); return rect; } @Override public Rectangle computeProximityRect(Rectangle currClientArea) { if (this.fScrollBar == null || !this.getVisible()) { return null; } int lineWidth = getMouseNearScrollScrollBarWidth(); int w = currClientArea.width; int h = currClientArea.height; Rectangle rect = new Rectangle(0, h - lineWidth, w, lineWidth); rect.height += 30; rect.y -= 15; return rect; } @Override protected int getRelevantPositionFromPos(Point styledTextPos) { return styledTextPos.x; } @Override public boolean computePositions(Rectangle currClientArea, Scrollable scrollable) { fHandleDrawnRect = null; if (this.fScrollBar == null || this.fScrollBar.getMaximum() - this.fScrollBar.getMinimum() <= 1 || !getVisible() || !this.fScrollBarSettings.getScrollBarThemed()) { return false; } StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width - (styledText.getLeftMargin() + styledText.getRightMargin()); int h = currClientArea.height; fScrollBarPositions = new ScrollBarPositions.ScrollBarPositionsHorizontal(this.fScrollBar.getMinimum(), this.fScrollBar.getMaximum(), styledText.getHorizontalPixel(), h, w); fHandleDrawnRect = fScrollBarPositions.getHandleDrawRect(lineWidth); if (fHandleDrawnRect == null || w <= fHandleDrawnRect.width) { return false; } return true; } @Override public void doPaintControl(GC gc, Rectangle currClientArea, Scrollable scrollable) { if (fHandleDrawnRect != null) { StyledText styledText = (StyledText) scrollable; int lineWidth = getCurrentScrollBarWidth(); int w = currClientArea.width - (styledText.getLeftMargin() + styledText.getRightMargin()); int h = currClientArea.height; int borderRadius = Math.min(fScrollBarSettings.getScrollBarBorderRadius(), lineWidth); // Fill the background (same thing as the // getFullBackgroundRect). gc.fillRoundRectangle(styledText.getLeftMargin(), h - lineWidth, w, lineWidth, borderRadius, borderRadius); // Fill the foreground. Color foreground = gc.getForeground(); Color background = gc.getBackground(); gc.setBackground(foreground); gc.fillRoundRectangle(fHandleDrawnRect.x, fHandleDrawnRect.y, fHandleDrawnRect.width, fHandleDrawnRect.height, borderRadius, borderRadius); gc.setBackground(background); } } /** * This function is overridden to force an instantaneous redraw in the * StyledText whenever we change the selection on the horizontal * scrollbar. */ @Override protected void notifyScrollbarSelectionChanged(Scrollable scrollable, int detail) { super.notifyScrollbarSelectionChanged(scrollable, detail); StyledText styledText = (StyledText) scrollable; syncStyledTextDrawing(styledText); } } /** * Paints the scrolls as needed (using internal handlers). */ public static class StyledTextPaintListener implements IScrollBarPainter { private final int fInitialBottomMargin; private final int fInitialRightMargin; private StyledText fStyledText; private boolean fInDraw; private Rectangle fCurrClientArea; private AbstractScrollHandler fHorizontalScrollHandler; private AbstractScrollHandler fVerticalScrollHandler; private IScrollBarSettings fScrollBarSettings; private Rectangle fLastHorizontalHandleRect; private Rectangle fLastVerticalHandleRect; public StyledTextPaintListener(AbstractScrollHandler horizontalScrollHandler, AbstractScrollHandler verticalScrollHandler, IScrollBarSettings colorProvider, int initialBottomMargin, int initialRightMargin) { this.fHorizontalScrollHandler = horizontalScrollHandler; this.fVerticalScrollHandler = verticalScrollHandler; this.fScrollBarSettings = colorProvider; this.fInitialBottomMargin = initialBottomMargin; this.fInitialRightMargin = initialRightMargin; } @Override public void install(Scrollable scrollable) { fStyledText = (StyledText) scrollable; } @Override public void uninstall() { fStyledText = null; fCurrClientArea = null; } @SuppressWarnings("unused") @Override public void paintControl(PaintEvent e) { if (fInDraw || fStyledText == null || fStyledText.isDisposed()) { return; } try { fInDraw = true; boolean clientAreaChanged = clientAreaChangedFromLastCall(); int charCount = fStyledText.getCharCount(); if (charCount <= 1) { return; } if (fCurrClientArea == null || fCurrClientArea.width < fVerticalScrollHandler.getMouseNearScrollScrollBarWidth() || fCurrClientArea.height < fHorizontalScrollHandler.getMouseNearScrollScrollBarWidth()) { return; } boolean drawHorizontal = fHorizontalScrollHandler.computePositions(fCurrClientArea, fStyledText); boolean drawVertical = fVerticalScrollHandler.computePositions(fCurrClientArea, fStyledText); if (!drawHorizontal && !drawVertical) { return; } fixMargins(drawHorizontal, drawVertical); try (AutoCloseable temp = configGC(e.gc)) { Rectangle clipping = e.gc.getClipping(); boolean redrawAsync = false; if (drawHorizontal) { Rectangle handleRect = fHorizontalScrollHandler.getHandleRect(); if (!handleRect.equals(fLastHorizontalHandleRect)) { if (clipping.intersection(handleRect).height != handleRect.height) { // Note: fixing the clipping area does not work. // We have to ask for a redraw of the component! redrawAsync = true; } } fLastHorizontalHandleRect = handleRect; fHorizontalScrollHandler.paintControl(e.gc, fCurrClientArea, fStyledText); } if (drawVertical) { Rectangle handleRect = fVerticalScrollHandler.getHandleRect(); if (!handleRect.equals(fLastVerticalHandleRect)) { if (clipping.intersection(handleRect).width != handleRect.width) { // Note: fixing the clipping area does not work. // We have to ask for a redraw of the component! redrawAsync = true; } } fLastVerticalHandleRect = handleRect; fVerticalScrollHandler.paintControl(e.gc, fCurrClientArea, fStyledText); } if (redrawAsync) { redrawAsync(); } } } catch (Exception e1) { CSSActivator.getDefault().log(LogService.LOG_ERROR, "Error painting scrollbar", e1); } finally { fInDraw = false; } } /** * Asynchronously asks for a redraw of the whole StyledText. */ private void redrawAsync() { Display.getCurrent().asyncExec(new Runnable() { @Override public void run() { if (fStyledText != null && !fStyledText.isDisposed()) { fStyledText.redraw(); } } }); } /** * Fixes the editor margins. The scrollbars always have to be drawn in * the editor margins (to avoid having the cursor over the scrollbar). * * @param drawHorizontal * Whether the horizontal scrollbar will be drawn. * @param drawVertical * Whether the vertical scrollbar will be drawn. * @return true if the margins have to be fixed before the actual * drawing and false otherwise. */ private boolean fixMargins(boolean drawHorizontal, boolean drawVertical) { int rightMargin; if (!drawVertical) { rightMargin = fInitialRightMargin; } else { int verticalLineWidth = fVerticalScrollHandler.getCurrentScrollBarWidth(); rightMargin = fInitialRightMargin < verticalLineWidth ? verticalLineWidth : fInitialRightMargin; if (fVerticalScrollHandler.fScrollBar == null || !fVerticalScrollHandler.getVisible()) { rightMargin = fStyledText.getRightMargin(); } } int bottomMargin; if (!drawHorizontal) { bottomMargin = fInitialBottomMargin; } else { int horizontalLineWidth = fHorizontalScrollHandler.getCurrentScrollBarWidth(); bottomMargin = fInitialBottomMargin < horizontalLineWidth ? horizontalLineWidth : fInitialBottomMargin; if (fHorizontalScrollHandler.fScrollBar == null || !fHorizontalScrollHandler.getVisible()) { bottomMargin = fStyledText.getBottomMargin(); } } if (fStyledText.getRightMargin() != rightMargin || fStyledText.getBottomMargin() != bottomMargin) { final int applyRightMargin = rightMargin; final int applyBottomMargin = bottomMargin; Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (fStyledText != null && !fStyledText.isDisposed()) { fStyledText.setMargins(fStyledText.getLeftMargin(), fStyledText.getTopMargin(), applyRightMargin, applyBottomMargin); } } }); return true; } return false; } /** * Configures the GC with the parameters needed for drawing. * * @param gc * @return an AutoCloseable (to be used in a try() statement) which will * restore the GC with the previous values. */ private AutoCloseable configGC(final GC gc) { final int oldLineStyle = gc.getLineStyle(); final int oldAlpha = gc.getAlpha(); final Color oldForeground = gc.getForeground(); final Color oldBackground = gc.getBackground(); final int oldLineWidth = gc.getLineWidth(); final int oldAntialias = gc.getAntialias(); Color foreground = fScrollBarSettings.getForegroundColor(); if (foreground != null) { gc.setForeground(foreground); } Color background = fScrollBarSettings.getBackgroundColor(); if (background != null) { gc.setBackground(background); } gc.setLineStyle(SWT.LINE_SOLID); gc.setAntialias(SWT.ON); gc.setLineWidth(1); return new AutoCloseable() { @Override public void close() throws Exception { gc.setForeground(oldForeground); gc.setBackground(oldBackground); gc.setAlpha(oldAlpha); gc.setLineStyle(oldLineStyle); gc.setLineWidth(oldLineWidth); gc.setAntialias(oldAntialias); } }; } /** * @return true if the client are changed from the last time this method * was called (and false otherwise). Has a side effect of * updating fCurrClientArea. */ private boolean clientAreaChangedFromLastCall() { Rectangle clientArea = fStyledText.getClientArea(); if (fCurrClientArea == null || !fCurrClientArea.equals(clientArea)) { fCurrClientArea = clientArea; return true; } return false; } @Override public void redrawScrollBars() { if (Display.getCurrent() != null && fStyledText != null) { if (!fStyledText.isDisposed() && fStyledText.isVisible()) { Rectangle clientArea = fStyledText.getClientArea(); fStyledText.redraw(clientArea.x, clientArea.y, clientArea.width, clientArea.height, false); } } } } /** * May return null if a StyledTextThemedScrollBarAdapter is already set in * the data. * * @param styledText * the StyledText for which the adapter is being requested. * @return the adapter or null if the data is already set with a * non-compatible instance. */ public static StyledTextThemedScrollBarAdapter getScrollbarAdapter(StyledText styledText) { if (styledText.getData("StyledTextThemedScrollBarAdapter") == null) { //$NON-NLS-1$ StyledTextThemedScrollBarAdapter scrollbarOnlyWhenNeeded = new StyledTextThemedScrollBarAdapter(styledText); styledText.setData("StyledTextThemedScrollBarAdapter", scrollbarOnlyWhenNeeded); //$NON-NLS-1$ return scrollbarOnlyWhenNeeded; } Object data = styledText.getData("StyledTextThemedScrollBarAdapter"); //$NON-NLS-1$ if (data instanceof StyledTextThemedScrollBarAdapter) { return (StyledTextThemedScrollBarAdapter) data; } return null; } }