/*******************************************************************************
* MontiCore Language Workbench
* Copyright (c) 2015, 2016, MontiCore, All rights reserved.
*
* This project is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this project. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package de.monticore.editorconnector;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.GraphicalViewer;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.views.contentoutline.ContentOutline;
import de.monticore.ast.ASTNode;
import de.monticore.genericgraphics.GenericFormEditor;
import de.monticore.genericgraphics.GenericGraphicsEditor;
import de.monticore.genericgraphics.GenericGraphicsViewer;
import de.monticore.genericgraphics.controller.editparts.IMCGraphicalEditPart;
import de.monticore.genericgraphics.controller.editparts.connections.IMCConnectionEditPart;
import de.monticore.genericgraphics.controller.selection.IHasLineNumbers;
import de.monticore.genericgraphics.controller.selection.SelectionSyncException;
import de.se_rwth.commons.SourcePosition;
import de.se_rwth.commons.logging.Log;
import de.se_rwth.langeditor.texteditor.TextEditorImpl;
/**
* <p>
* This class provides an {@link ISelectionListener} for {@link TextEditorImpl TextEditorImpls} and
* {@link GenericGraphicsEditor GenericGraphicsEditors}. It synchronizes the selection of both, that
* means, if something is selected in the text its also selected in the graphical editor and vice
* versa.<br>
* <br>
* It assumes that only one {@link TextEditorImpl} and one {@link GenericGraphicsEditor} are opened
* on the same file.
* </p>
* <p>
* Therefore is listens for selection changes and synchronizes them as follows:
* <ul>
* <li>Rouhly, a mapping from text position to EditParts and a mapping from EditParts to text
* positions is created. It is created on the basis of
* <ul>
* <li>{@link ASTNode ASTNodes} that save their linenumbers</li>
* <li>models that implement the {@link IHasLineNumbers} interface</li>
* </ul>
* </li>
* <li>Whenever one (or more) {@link GraphicalEditPart GraphicalEditPart(s)} are selected in a
* {@link GenericGraphicsEditor}, an {@link TextEditorImpl TextEditorImpls} with the same file in
* the workbench is searched. If found, the selection is set accordingly.</li> *
* <li>Whenever one (or more) line(s) in a {@link TextEditorImpl} is (are) selected, it's checked if
* this listener is registered to the {@link GenericGraphicsEditor} with the same file as input. If
* so, the selection is set accordingly.</li>
* <li>Whenever one (or more) objects in a {@link ContentOutline} is (are) selected, it's checked if
* this listener is registered to the {@link GenericGraphicsEditor} with the same file as input. If
* so, the selection is set accordingly.</li>
* </ul>
* </p>
* TODO: Make it faster! Performance is low, perhaps some cached values?
*
* @author Tim Enger
*/
public class GraphicalSelectionListener implements ISelectionListener {
private GenericGraphicsViewer viewer;
private IFile ownFile;
private ISelection ownSelection;
// mapping from line numbers to all editparts
// that have an associated model object, that
// has this line number in the textual representation
// in addition to only saving the mapping of line -> editpart
// also associate the exact position in the model
private Map<Integer, List<IMCGraphicalEditPart>> linesToEP = new LinkedHashMap<Integer, List<IMCGraphicalEditPart>>();;
// mapping from editparts to position in textual source of the model
private Map<IMCGraphicalEditPart, TextPosition> epToPos = new LinkedHashMap<IMCGraphicalEditPart, TextPosition>();;
/**
* Constructor
*
* @param ownFile The {@link IFile} the editor is opened on.
* @param viewer The {@link GraphicalViewer} this listener is based on.
* @throws SelectionSyncException
*/
public GraphicalSelectionListener(IFile ownFile, GenericGraphicsViewer viewer) {
this.viewer = viewer;
this.ownFile = ownFile;
}
@Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
if (selection.equals(ownSelection)) {
return;
}
if (selection instanceof IStructuredSelection) {
IStructuredSelection ss = (IStructuredSelection) selection;
if (part instanceof GenericFormEditor) {
IFile file = getFile(part);
if (file == null || !file.equals(ownFile)) {
return;
}
setSelection(ss, ((GenericFormEditor) part).getTextEditor());
}
else if (part instanceof ContentOutline) {
// selection originates from agraphical OutlinePage
ContentOutline co = (ContentOutline) part;
IEditorPart activeE = co.getViewSite().getPage().getActiveEditor();
if (activeE instanceof TextEditorImpl) {
setSelection(ss, (TextEditorImpl) activeE);
}
}
}
else if (selection instanceof ITextSelection) {
ITextSelection ts = (ITextSelection) selection;
if (part instanceof GenericFormEditor) {
IFile file = getFile(part);
if (file == null || !file.equals(ownFile)) {
return;
}
setSelection(ts, (GenericFormEditor) part);
}
else if (part instanceof ContentOutline) {
// selection originates from a textual OutlinePage
// Nothing to do: Handled in OutlinePage
}
}
}
/**
* Create the mappings needed for computation of selections:
* <ul>
* <li><code>epToPos</code>: Map from {@link IMCGraphicalEditPart IMCGraphicalEditParts} to
* {@link TextPosition TextPositions}. <br>
* Thereby, {@link TextPosition TextPositions} are computed based on the model object which should
* be either an {@link ASTNode} or an {@link IHasLineNumbers}.</li>
* <li><code>linesToEPToPos</code>: A map from line numbers to all {@link IMCGraphicalEditPart
* IMCGraphicalEditParts} whose underlying model objects have this line number</li>
* </ul>
*
* @throws SelectionSyncException
*/
public void createMappings() throws SelectionSyncException {
epToPos.clear();
linesToEP.clear();
for (Object ep : viewer.getEditPartRegistry().values()) {
if (ep instanceof IMCGraphicalEditPart) {
addMapping((IMCGraphicalEditPart) ep);
}
}
}
private void addMapping(IMCGraphicalEditPart ep) {
Object model = ep.getModel();
if (model instanceof ASTNode) {
ASTNode node = (ASTNode) ep.getModel();
addMapping(node, ep);
}
else if (model instanceof IHasLineNumbers) {
IHasLineNumbers o = (IHasLineNumbers) ep.getModel();
addMapping(o, ep);
}
}
private void addMapping(ASTNode node, IMCGraphicalEditPart ep) {
SourcePosition sps = node.get_SourcePositionStart();
SourcePosition spe = node.get_SourcePositionEnd();
if (sps == null || spe == null) {
return;
}
TextPosition tp = new TextPosition(sps.getLine(), sps.getColumn(), spe.getLine(),
spe.getColumn());
epToPos.put(ep, tp);
for (int line = sps.getLine(); line <= spe.getLine(); line++) {
addToMap(line, ep, tp);
}
}
private void addMapping(IHasLineNumbers o, IMCGraphicalEditPart ep) {
TextPosition tp = new TextPosition(o.getStartLine(), o.getStartOffset(), o.getEndLine(),
o.getEndOffset());
epToPos.put(ep, tp);
for (int line = o.getStartLine(); line <= o.getEndLine(); line++) {
addToMap(line, ep, tp);
}
}
private void addToMap(int line, IMCGraphicalEditPart ep, TextPosition tp) {
// Map<Integer, Map<IMCGraphicalEditPart, TextPosition>>
List<IMCGraphicalEditPart> listForLine = linesToEP.get(line);
if (listForLine == null) {
listForLine = new ArrayList<IMCGraphicalEditPart>();
linesToEP.put(line, listForLine);
}
listForLine.add(ep);
}
/**
* <p>
* Sets the text selection in a {@link TextEditorImpl} according to the selected
* {@link GraphicalEditPart GraphicalEditParts} in the {@link IStructuredSelection}.
* </p>
* <p>
* The selection is set according to the editpart {@link TextPosition TextPositions}. <br>
* <br>
* If only one is selected, the selection is precisely only the {@link TextPosition}.<br>
* If more than one is selected, the selection goes from the lowest start position to the highest
* end position in the text editor.
* </p>
*
* @param ss The {@link IStructuredSelection} containing the selected {@link GraphicalEditPart
* GraphicalEditParts}.
* @param editor The {@link TextEditorImpl} to set the text selection in.
*/
@SuppressWarnings("unchecked")
private void setSelection(IStructuredSelection ss, TextEditorImpl editor) {
List<Object> selectionList = ss.toList();
// if there is more than one element selected select all
// from minimum line number with minimum offset
// to maximum line number with maximum offset
// works also for only one element selected
int startLine = Integer.MAX_VALUE;
int startOffset = Integer.MAX_VALUE;
int endLine = Integer.MIN_VALUE;
int endOffset = Integer.MIN_VALUE;
boolean setOnce = false;
for (Object o : selectionList) {
if (o instanceof IMCGraphicalEditPart) {
IMCGraphicalEditPart ep = (IMCGraphicalEditPart) o;
if (!ep.isSelectable()) {
continue;
}
if (ep.getModel() instanceof ASTNode) {
ASTNode astNode = (ASTNode) ep.getModel();
startLine = Math.min(startLine, astNode.get_SourcePositionStart().getLine());
startOffset = Math.min(startOffset, astNode.get_SourcePositionStart().getColumn());
endLine = Math.max(endLine, astNode.get_SourcePositionEnd().getLine());
endOffset = Math.max(endOffset, astNode.get_SourcePositionEnd().getColumn());
setOnce = true;
}
}
}
if (setOnce) {
selectText(editor, new TextPosition(startLine, startOffset, endLine, endOffset));
}
}
/**
* Select text in text {@link TextEditorImpl} according to {@link TextPosition}.
*
* @param editor The {@link TextEditorImpl}
* @param tp The {@link TextPosition}
*/
private void selectText(TextEditorImpl editor, TextPosition tp) {
ISelectionProvider sp = editor.getSelectionProvider();
// if Textpos is null, select nothing
if (tp == null) {
return;
}
// we have to find out which offset the lines have, we want to highlight
IDocumentProvider dp = editor.getDocumentProvider();
IDocument doc = dp.getDocument(editor.getEditorInput());
int offset = -1;
int length = -1;
try {
// offset of first character of the line
// monticore starts counting from 1, eclipse from 0
offset = doc.getLineOffset(tp.getStartLine() - 1);
offset += tp.getStartOffset();
// monticore starts counting from 1, eclipse from 0
offset--;
// the offset of the endline
length = doc.getLineOffset(tp.getEndLine() - 1);
length += tp.getEndOffset();
// substract the offset from start
length -= offset;
// substract one more, to ensure that not the whole
// line at the end is selected
// it should be only selected till the last character
length--;
}
catch (BadLocationException e) {
Log.error("0xA1101 Text Selection failed due to the following exception: " + e);
return;
}
// do the selection
if (offset > -1 && length > -1) {
ownSelection = new TextSelection(offset, length);
sp.setSelection(ownSelection);
}
}
/**
* Sets the graphical selection in the {@link GenericGraphicsEditor} according to the selected
* text in the {@link ITextSelection}.
*
* @param ts The {@link ITextSelection} containing the selected text
* @param editor The {@link TextEditorImpl} needed to compute offset.
*/
private void setSelection(ITextSelection ts, GenericFormEditor editor) {
// we have to find out which offset the lines have, we want to highlight
IDocumentProvider dp = editor.getTextEditor().getDocumentProvider();
IDocument doc = dp.getDocument(editor.getEditorInput());
// determine the current text selection
int startLine = ts.getStartLine();
int endLine = ts.getEndLine();
int startOffset = -1;
int endOffset = -1;
try {
startOffset = ts.getOffset() - doc.getLineOffset(startLine);
endOffset = ts.getOffset() + ts.getLength() - doc.getLineOffset(endLine);
}
catch (BadLocationException e) {
Log.error("0xA1102 Text Selection failed due to the following exception: " + e);
return;
}
// monticore starts counting at 1, eclipse at 0
startLine++;
endLine++;
startOffset++;
endOffset++;
selectEPFromPosition(startLine, startOffset, endLine, endOffset);
}
private void selectEPFromPosition(int startLine, int startOffset, int endLine, int endOffset) {
// collect all potential editparts
List<IMCGraphicalEditPart> potEPs = new ArrayList<>();
List<IMCGraphicalEditPart> epsToSelect = new ArrayList<>();
for (int line = startLine; line <= endLine; line++) {
List<IMCGraphicalEditPart> epsList = linesToEP.get(line);
if (epsList != null) {
potEPs.addAll(epsList);
}
}
// check if complete text position of EP
// is included in the selection, and EP is selectable
// if so select it
for (IMCGraphicalEditPart ep : potEPs) {
TextPosition tp = epToPos.get(ep);
if (tp.isIncluded(startLine, startOffset, endLine, endOffset)) {
if (ep.isSelectable()) {
epsToSelect.add(ep);
}
}
}
// check if something will be selected
if (epsToSelect.isEmpty()) {
// so nothing will be selected,
// try to find the smallest encompassing editpart
IMCGraphicalEditPart smallestEP = null;
TextPosition smallestTP = null;
for (IMCGraphicalEditPart ep : potEPs) {
TextPosition tp = epToPos.get(ep);
if (tp.isEncompassing(startLine, startOffset, endLine, endOffset)) {
if (smallestEP == null) {
smallestEP = ep;
smallestTP = tp;
}
else {
// if this text position is encompassed by the current
// smallest one then this one is the new smallest one
if (smallestTP.isEncompassing(tp.getStartLine(), tp.getStartOffset(), tp.getEndLine(),
tp.getEndOffset())) {
// except the new one is a IMCConnectionEP (which is not part of
// an association)
// this avoids the selection of "extends"/"implements"
// connections instead of the corresponding classes
if (!(ep instanceof IMCConnectionEditPart)
|| ep.getIdentifier().startsWith("association")) {
smallestEP = ep;
smallestTP = tp;
}
}
}
}
}
if (smallestEP != null) {
if (smallestEP.isSelectable()) {
epsToSelect.add(smallestEP);
}
}
}
selectEditParts(epsToSelect);
}
/**
* Select the {@link GraphicalEditPart GraphicalEditParts} in the list. And sets the focus (
* {@link GraphicalViewer#reveal(org.eclipse.gef.EditPart)} to the first one.
*
* @param selection List of {@link GraphicalEditPart GraphicalEditParts} to select.
*/
private void selectEditParts(List<IMCGraphicalEditPart> selection) {
ownSelection = new StructuredSelection(selection);
viewer.setSelection(ownSelection);
// set focus to the first one
if (!selection.isEmpty()) {
if (viewer.getControl() != null)
viewer.reveal(selection.get(0));
}
}
/**
* @param part {@link IWorkbenchPart}
* @return The input {@link IFile} of the {@link IWorkbenchPart}.
*/
private IFile getFile(IWorkbenchPart part) {
if (part instanceof IEditorPart) {
return getFileForIEditorPart((IEditorPart) part);
}
return null;
}
private IFile getFileForIEditorPart(IEditorPart part) {
if (part.getEditorInput() instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) part.getEditorInput()).getFile();
return file;
}
return null;
}
private class TextPosition {
private int startLine;
private int endLine;
private int startOffset;
private int endOffset;
/**
* @param startLine
* @param startOffset
* @param endLine
* @param endOffset
*/
public TextPosition(int startLine, int startOffset, int endLine, int endOffset) {
this.startLine = startLine;
this.endLine = endLine;
this.startOffset = startOffset;
this.endOffset = endOffset;
}
/**
* @return The startLine
*/
public int getStartLine() {
return startLine;
}
/**
* @return The endLine
*/
public int getEndLine() {
return endLine;
}
/**
* @return The startOffset
*/
public int getStartOffset() {
return startOffset;
}
/**
* @return The endOffset
*/
public int getEndOffset() {
return endOffset;
}
/**
* Determines if {@link TextPosition this} is included in the given values.
*
* @param startLine The start line
* @param startOffset The start offset
* @param endLine The end line
* @param endOffset The end offset
* @return <tt>True</tt> {@link TextPosition this} is included in the given values.
*/
public boolean isIncluded(int startLine, int startOffset, int endLine, int endOffset) {
int tpStartLine = getStartLine();
int tpEndLine = getEndLine();
if (tpStartLine >= startLine && tpEndLine <= endLine) {
boolean start;
// pay attention to offset
if (tpStartLine == startLine) {
start = getStartOffset() >= startOffset;
}
else {
start = true;
}
if (start && tpEndLine == endLine) {
return getEndOffset() <= endOffset;
}
if (start) {
return true;
}
}
return false;
}
/**
* Determines if {@link TextPosition this} encompasses the given values.
*
* @param startLine The start line
* @param startOffset The start offset
* @param endLine The end line
* @param endOffset The end offset
* @return <tt>True</tt> {@link TextPosition this} encompassed the given values.
*/
public boolean isEncompassing(int startLine, int startOffset, int endLine, int endOffset) {
// check the lines
if (getStartLine() <= startLine && getEndLine() >= endLine) {
boolean startOffsetGood = true;
boolean endOffsetGood = true;
// if on the same start line, the offsets need to be checked
if (getStartLine() == startLine) {
startOffsetGood = getStartOffset() <= startOffset;
}
// if on the same end line, the offsets need to be checked
if (getEndLine() == endLine) {
endOffsetGood = getEndOffset() >= endOffset;
}
if (startOffsetGood && endOffsetGood) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "start: " + startLine + ":" + startOffset + " -> end: " + endLine + ":" + endOffset;
}
}
}