blob: 0aaea1b31bb000b842592dd87afc9fddff01048c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018 Angelo ZERR.
* 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:
* Angelo Zerr <angelo.zerr@gmail.com> - [minimap] Initialize minimap view - Bug 535450
* Arne Deutsch <arne.deutsch@itemis.de> - Correct view height - Bug 536207
*******************************************************************************/
package org.eclipse.ui.internal.views.minimap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.TextChangeListener;
import org.eclipse.swt.custom.TextChangedEvent;
import org.eclipse.swt.custom.TextChangingEvent;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextPresentationListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension4;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.IViewportListener;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.TextViewer;
/**
* Minimap widget which displays scaled content of the given text editor.
*
*/
public class MinimapWidget {
private static final float SCALE = 0.2f;
private final ITextViewer fEditorViewer;
private final StyledText fMinimapTextWidget;
/**
* Editor tracker used to track text changed and styles changes of the
* editor content.
*/
class EditorTracker implements TextChangeListener, ControlListener, ITextPresentationListener, IViewportListener {
private Map<Font, Font> fScaledFonts;
@Override
public void textSet(TextChangedEvent event) {
synchText();
}
@Override
public void textChanging(TextChangingEvent event) {
fMinimapTracker.replaceTextRange(event);
}
@Override
public void textChanged(TextChangedEvent event) {
updateMinimapAfterTextChange();
}
@Override
public void applyTextPresentation(TextPresentation presentation) {
addPresentation(presentation);
}
private StyleRange modelStyleRange2WidgetStyleRange(StyleRange range) {
IRegion region = modelRange2WidgetRange(new Region(range.start, range.length));
if (region != null) {
StyleRange result = (StyleRange) range.clone();
result.start = region.getOffset();
result.length = region.getLength();
return result;
}
return null;
}
private IRegion modelRange2WidgetRange(IRegion region) {
if (fEditorViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5) fEditorViewer;
return extension.modelRange2WidgetRange(region);
}
if (fEditorViewer instanceof TextViewer) {
return ((TextViewer) fEditorViewer).modelRange2WidgetRange(region);
}
IRegion visibleRegion = fEditorViewer.getVisibleRegion();
int start = region.getOffset() - visibleRegion.getOffset();
int end = start + region.getLength();
if (end > visibleRegion.getLength())
end = visibleRegion.getLength();
return new Region(start, end - start);
}
private void addPresentation(TextPresentation presentation) {
StyleRange range = presentation.getDefaultStyleRange();
if (range != null) {
range = modelStyleRange2WidgetStyleRange(range);
if (range != null) {
updateStyle(range);
fMinimapTextWidget.setStyleRange(range);
}
List<StyleRange> ranges = new ArrayList<>(presentation.getDenumerableRanges());
Iterator<StyleRange> e = presentation.getNonDefaultStyleRangeIterator();
while (e.hasNext()) {
range = e.next();
range = modelStyleRange2WidgetStyleRange(range);
if (range != null) {
updateStyle(range);
ranges.add(range);
}
}
if (!ranges.isEmpty()) {
fMinimapTextWidget.replaceStyleRanges(0, 0, ranges.toArray(new StyleRange[ranges.size()]));
}
} else {
IRegion region = modelRange2WidgetRange(presentation.getCoverage());
if (region == null)
return;
List<StyleRange> list = new ArrayList<>(presentation.getDenumerableRanges());
Iterator<StyleRange> e = presentation.getAllStyleRangeIterator();
while (e.hasNext()) {
range = e.next();
range = modelStyleRange2WidgetStyleRange(range);
if (range != null) {
updateStyle(range);
list.add(range);
}
}
if (!list.isEmpty()) {
StyleRange[] ranges = new StyleRange[list.size()];
list.toArray(ranges);
fMinimapTextWidget.replaceStyleRanges(region.getOffset(), region.getLength(), ranges);
}
}
}
void updateStyle(StyleRange range) {
// scale the font
range.font = getScaledFont(range.font);
// remove GlyphMetrics (created by code mining)
range.metrics = null;
}
Font getScaledFont(Font editorFont) {
if (editorFont == null) {
return null;
}
Font scaledFont = fScaledFonts.get(editorFont);
if (scaledFont != null) {
return scaledFont;
}
FontData[] fontData = editorFont.getFontData();
FontData fontDatum = fontData[0];
scaledFont = new Font(editorFont.getDevice(), fontDatum.getName(),
(int) Math.ceil(fontDatum.getHeight() * getScale()), fontDatum.getStyle());
fScaledFonts.put(editorFont, scaledFont);
return scaledFont;
}
@Override
public void controlMoved(ControlEvent e) {
// Do nothing
}
@Override
public void controlResized(ControlEvent e) {
updateMinimapAfterResize();
}
@Override
public void viewportChanged(int verticalOffset) {
fMinimapTextWidget.getDisplay().asyncExec(() -> {
if (!fMinimapTextWidget.isDisposed()) {
updateMinimapAfterResize();
}
});
}
void updateMinimapAfterResize() {
updateMinimap(false);
}
void updateMinimapAfterTextChange() {
updateMinimap(true);
}
void updateMinimap(boolean textChanged) {
StyledText editorTextWidget = fEditorViewer.getTextWidget();
int editorTopIndex = JFaceTextUtil.getPartialTopIndex(editorTextWidget);
int editorBottomIndex = JFaceTextUtil.getPartialBottomIndex(editorTextWidget);
int maximalLines = fEditorViewer.getTextWidget().getClientArea().height
/ fEditorViewer.getTextWidget().getLineHeight();
fMinimapTracker.updateMinimap(editorTopIndex, editorBottomIndex, maximalLines, textChanged);
}
void install() {
StyledText editorTextWidget = fEditorViewer.getTextWidget();
fScaledFonts = new HashMap<>();
// Compute scaled font
Font scaledFont = getScaledFont(editorTextWidget.getFont());
fMinimapTextWidget.setFont(scaledFont);
// track changed content of styled text of the editor
editorTextWidget.getContent().addTextChangeListener(this);
// track changed styles of styled text of the editor
fMinimapTextWidget.setBackground(editorTextWidget.getBackground());
fMinimapTextWidget.setForeground(editorTextWidget.getForeground());
if (fEditorViewer instanceof ITextViewerExtension4) {
((ITextViewerExtension4) fEditorViewer).addTextPresentationListener(this);
}
// track changed of vertical bar scroll to update highlight
// Viewport.
fEditorViewer.addViewportListener(this);
editorTextWidget.addControlListener(this);
synchTextAndStyles();
}
void synchTextAndStyles() {
synchText();
synchStyles();
}
private void synchStyles() {
StyledText editorTextWidget = fEditorViewer.getTextWidget();
StyleRange[] ranges = editorTextWidget.getStyleRanges();
if (ranges != null) {
for (StyleRange range : ranges) {
updateStyle(range);
}
}
fMinimapTextWidget.setStyleRanges(ranges);
}
private void synchText() {
StyledText editorTextWidget = fEditorViewer.getTextWidget();
fMinimapTextWidget.setText(editorTextWidget.getText());
}
void uninstall() {
StyledText editorTextWidget = fEditorViewer.getTextWidget();
// untrack changed content of styled text of the editor
if (editorTextWidget.getContent() != null) {
editorTextWidget.getContent().removeTextChangeListener(this);
}
// untrack changed styles of styled text of the editor
if (fEditorViewer instanceof ITextViewerExtension4) {
((ITextViewerExtension4) fEditorViewer).removeTextPresentationListener(this);
}
// track changed of vertical bar scroll to update highlight
// Viewport.
fEditorViewer.removeViewportListener(this);
editorTextWidget.removeControlListener(this);
fScaledFonts.values().forEach(Font::dispose);
}
}
/**
* Minimap tracker.
*
*/
class MinimapTracker implements CaretListener, SelectionListener, PaintListener, MouseWheelListener {
private static final int NB_LINES_SCROLL = 10;
private int fEditorTopIndex;
private int fEditorBottomIndex;
private int fTopIndexY;
private int fBottomIndexY;
private int fMinimalHeight;
private int fMaximalLines;
private boolean fTextChanging;
@Override
public void caretMoved(CaretEvent event) {
if (fTextChanging) {
// When editor content changed, it updates the minimap styled
// text content which update caret.
// In this case, ignore it to avoid set the top index of the
// editor viewer.
return;
}
updateEditorTopIndex();
}
private void updateEditorTopIndex() {
int caretOffset = fMinimapTextWidget.getCaretOffset();
int lineAtOffset = fMinimapTextWidget.getLineAtOffset(caretOffset);
int newTopIndex = lineAtOffset;
if (fEditorViewer instanceof ITextViewerExtension5) {
// adjust offset according folded content
newTopIndex = ((ITextViewerExtension5) fEditorViewer).widgetLine2ModelLine(lineAtOffset);
}
fEditorViewer.setTopIndex(newTopIndex);
}
void updateMinimap(int editorTopIndex, int editorBottomIndex, int maximalLines, boolean textChanged) {
if (editorTopIndex != fEditorTopIndex || editorBottomIndex != fEditorBottomIndex
|| maximalLines != fMaximalLines || textChanged) {
fEditorTopIndex = editorTopIndex;
fEditorBottomIndex = editorBottomIndex;
fMaximalLines = maximalLines;
// Update the position of minimap styled text
fMinimapTextWidget.setRedraw(false);
int newMinimapTopIndex = editorTopIndex;
if (editorTopIndex != 0
&& editorBottomIndex != fMinimapTextWidget.getLineAtOffset(fMinimapTextWidget.getCharCount())) {
// center the draw of square of editor client area
int minimapTopIndex = JFaceTextUtil.getPartialTopIndex(fMinimapTextWidget);
int minimapBottomIndex = JFaceTextUtil.getPartialBottomIndex(fMinimapTextWidget);
int minimapVisibleLineCount = minimapBottomIndex - minimapTopIndex;
int editorVisibleLineCount = editorBottomIndex - editorTopIndex;
newMinimapTopIndex = Math.max(0,
editorTopIndex + editorVisibleLineCount - minimapVisibleLineCount / 2);
}
fMinimapTextWidget.setTopIndex(newMinimapTopIndex);
fTopIndexY = fMinimapTextWidget.getLinePixel(fEditorTopIndex);
// "fEditorBottomIndex + 1" because fEditorBottomIndex is START
// of last line ... we want to include it
fBottomIndexY = fMinimapTextWidget.getLinePixel(fEditorBottomIndex + 1);
// to avoid that the highlight viewport shrinks to the text
// height in case all content is visible in the editor,
// calculate a minimal height based on the number of lines
// maximal shown and the line height in the minimap
fMinimalHeight = maximalLines > fMinimapTextWidget.getLineCount()
? maximalLines * fMinimapTextWidget.getLineHeight()
: 0;
fMinimapTextWidget.setRedraw(true);
}
}
void replaceTextRange(TextChangingEvent event) {
fTextChanging = true;
int start = event.start;
int length = event.replaceCharCount;
String text = event.newText;
if (event.newLineCount > 0 || event.replaceLineCount > 0) {
Rectangle clientArea = fMinimapTextWidget.getClientArea();
fMinimapTextWidget.setRedraw(false);
fMinimapTextWidget.replaceTextRange(start, length, text);
fMinimapTextWidget.redraw(0, fTopIndexY, clientArea.width, clientArea.height, false);
fMinimapTextWidget.setRedraw(true);
} else {
fMinimapTextWidget.replaceTextRange(start, length, text);
}
fTextChanging = false;
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
// Do nothing
}
@Override
public void widgetSelected(SelectionEvent e) {
// hack to avoid drawing selection of styled text while mouse move
// of highlight viewport
fMinimapTextWidget.setSelection(fMinimapTextWidget.getCaretOffset());
}
@Override
public void paintControl(PaintEvent event) {
GC gc = event.gc;
int clientAreaWidth = fMinimapTextWidget.getClientArea().width;
gc.setBackground(fMinimapTextWidget.getSelectionBackground());
Rectangle rect = new Rectangle(0, fTopIndexY, clientAreaWidth,
Math.max(fBottomIndexY - fTopIndexY, fMinimalHeight));
gc.drawRectangle(rect.x, rect.y, Math.max(1, rect.width - 1), Math.max(1, rect.height - 1));
gc.setAdvanced(true);
if (gc.getAdvanced()) {
gc.setAlpha(20);
gc.fillRectangle(rect);
gc.setAdvanced(false);
}
}
@Override
public void mouseScrolled(MouseEvent e) {
int caretOffset = fMinimapTextWidget.getOffsetAtPoint(new Point(0, fTopIndexY));
int lineIndex = fMinimapTextWidget.getLineAtOffset(caretOffset);
if (e.count > 0) {
lineIndex = Math.max(0, lineIndex - NB_LINES_SCROLL);
} else {
lineIndex = Math.min(fMinimapTextWidget.getCharCount(), lineIndex + NB_LINES_SCROLL);
}
caretOffset = fMinimapTextWidget.getOffsetAtLine(lineIndex);
fMinimapTextWidget.setCaretOffset(caretOffset);
}
void install() {
fMinimapTextWidget.addCaretListener(this);
fMinimapTextWidget.addSelectionListener(this);
fMinimapTextWidget.addPaintListener(this);
fMinimapTextWidget.addMouseWheelListener(this);
}
void uninstall() {
if (!fMinimapTextWidget.isDisposed()) {
fMinimapTextWidget.removeCaretListener(this);
fMinimapTextWidget.removeSelectionListener(this);
fMinimapTextWidget.removePaintListener(this);
fMinimapTextWidget.removeMouseWheelListener(this);
}
}
}
private final EditorTracker fEditorTracker;
private final MinimapTracker fMinimapTracker;
public MinimapWidget(Composite parent, ITextViewer viewer) {
fEditorViewer = viewer;
// Create minimap styled text
fMinimapTextWidget = new StyledText(parent, SWT.MULTI | SWT.READ_ONLY);
fMinimapTextWidget.setEditable(false);
fMinimapTextWidget.setCursor(fMinimapTextWidget.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
// Initialize trackers
fEditorTracker = new EditorTracker();
fMinimapTracker = new MinimapTracker();
}
/**
* Install minimap widget.
*/
public void install() {
fEditorTracker.install();
fMinimapTracker.install();
}
public void uninstall() {
fEditorTracker.uninstall();
fMinimapTracker.uninstall();
}
public Control getControl() {
return fMinimapTextWidget;
}
float getScale() {
return SCALE;
}
}