package com.redhat.ceylon.eclipse.code.editor;
import static com.redhat.ceylon.eclipse.code.complete.CodeCompletions.getLabelDescriptionFor;
import static com.redhat.ceylon.eclipse.code.editor.Navigation.getNodePath;
import static com.redhat.ceylon.eclipse.code.editor.Navigation.gotoNode;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static com.redhat.ceylon.eclipse.ui.CeylonResources.CEYLON_SOURCE;
import static com.redhat.ceylon.eclipse.util.EditorUtil.getEditorInput;
import static com.redhat.ceylon.eclipse.util.Nodes.findNode;
import static com.redhat.ceylon.eclipse.util.Nodes.getReferencedNode;
import java.util.StringTokenizer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.text.IInformationControlExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.eclipse.code.parse.CeylonParseController;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
import com.redhat.ceylon.eclipse.util.EditorUtil;
import com.redhat.ceylon.eclipse.util.Highlights;
import com.redhat.ceylon.model.typechecker.model.Declaration;
final class PeekDefinitionPopup extends PopupDialog
implements IInformationControl, IInformationControlExtension2,
IInformationControlExtension3 {
private final class GotoListener implements KeyListener {
@Override
public void keyReleased(KeyEvent e) {}
@Override
public void keyPressed(KeyEvent e) {
if (e.character == 0x1B) // ESC
dispose();
if (EditorUtil.triggersBinding(e, getCommandBinding())) {
e.doit=false;
dispose();
gotoNode(referencedNode, editor);
}
}
}
private ISourceViewer viewer;
private final CeylonEditor editor;
private Node referencedNode;
private final CeylonParseController parseController =
new CeylonParseController();
private final IDocumentProvider docProvider =
new SourceArchiveDocumentProvider();
private IEditorInput ei;
public ISourceViewer getViewer() {
return viewer;
}
private StyledText titleLabel;
private TriggerSequence commandBinding;
protected TriggerSequence getCommandBinding() {
return commandBinding;
}
PeekDefinitionPopup(Shell parent, int shellStyle, CeylonEditor editor) {
super(parent, shellStyle, true, true, false, true,
true, null, null);
this.editor = editor;
commandBinding =
EditorUtil.getCommandBinding(PLUGIN_ID +
".editor.code");
if (commandBinding!=null) {
setInfoText(commandBinding.format() + " to open editor");
}
create();
}
private StyledText getEditorWidget(CeylonEditor editor) {
return editor.getCeylonSourceViewer().getTextWidget();
}
protected Control createContents(Composite parent) {
Composite composite = (Composite) super.createContents(parent);
GridLayout layout = (GridLayout) composite.getLayout();
layout.verticalSpacing=8;
layout.marginLeft=8;
layout.marginRight=8;
layout.marginTop=8;
layout.marginBottom=8;
Control[] children = composite.getChildren();
children[children.length-2].setVisible(false);
return composite;
}
@Override
protected Control createDialogArea(Composite parent) {
viewer = new CeylonSourceViewer(editor, parent, null, null, false,
SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.FULL_SELECTION);
viewer.configure(new CeylonSourceViewerConfiguration(editor) {
@Override
protected CeylonParseController getParseController() {
return parseController;
}
});
viewer.setEditable(false);
StyledText textWidget = viewer.getTextWidget();
textWidget.setFont(getEditorWidget(editor).getFont());
// textWidget.setBackground(getEditorWidget(editor).getBackground());
textWidget.addKeyListener(new GotoListener());
return textWidget;
}
private static GridLayoutFactory popupLayoutFactory;
protected static GridLayoutFactory getPopupLayout() {
if (popupLayoutFactory == null) {
popupLayoutFactory = GridLayoutFactory.fillDefaults()
.margins(POPUP_MARGINWIDTH, POPUP_MARGINHEIGHT)
.spacing(POPUP_HORIZONTALSPACING, POPUP_VERTICALSPACING);
}
return popupLayoutFactory;
}
protected StyledString styleTitle(final StyledText title) {
StyledString result = new StyledString();
StringTokenizer tokens =
new StringTokenizer(title.getText(), "\u2014", false);
styleDescription(title, result, tokens.nextToken());
result.append("\u2014");
Highlights.styleFragment(result,
tokens.nextToken(), false, null,
CeylonPlugin.getOutlineFont());
return result;
}
protected void styleDescription(final StyledText title, StyledString result,
String desc) {
final FontData[] fontDatas = title.getFont().getFontData();
for (int i = 0; i < fontDatas.length; i++) {
fontDatas[i].setStyle(SWT.BOLD);
}
result.append(desc, new Styler() {
@Override
public void applyStyles(TextStyle textStyle) {
textStyle.font=new Font(title.getDisplay(), fontDatas);
}
});
}
@Override
protected Control createTitleControl(Composite parent) {
getPopupLayout().copy()
.numColumns(3)
.spacing(6, 6)
.applyTo(parent);
Label iconLabel = new Label(parent, SWT.NONE);
iconLabel.setImage(CeylonPlugin.imageRegistry().get(CEYLON_SOURCE));
getShell().addKeyListener(new GotoListener());
titleLabel = new StyledText(parent, SWT.NONE);
titleLabel.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
titleLabel.setStyleRanges(styleTitle(titleLabel).getStyleRanges());
}
});
titleLabel.setEditable(false);
GridDataFactory.fillDefaults()
.align(SWT.FILL, SWT.CENTER)
.grab(true,false)
.span(1, 1)
.applyTo(titleLabel);
return null;
}
@Override
protected void setTitleText(String text) {
if (titleLabel!=null)
titleLabel.setText(text);
}
/*@Override
protected void adjustBounds() {
Rectangle bounds = getShell().getBounds();
int h = bounds.height;
if (h>400) {
bounds.height=400;
bounds.y = bounds.y + (h-400)/3;
getShell().setBounds(bounds);
}
int w = bounds.width;
if (w<600) {
bounds.width=600;
getShell().setBounds(bounds);
}
}*/
public void setInformation(String information) {
// this method is ignored, see IInformationControlExtension2
}
public void setSize(int width, int height) {
getShell().setSize(width, height);
}
public void addDisposeListener(DisposeListener listener) {
getShell().addDisposeListener(listener);
}
public void removeDisposeListener(DisposeListener listener) {
getShell().removeDisposeListener(listener);
}
public void setForegroundColor(Color foreground) {
applyForegroundColor(foreground, getContents());
}
public void setBackgroundColor(Color background) {
applyBackgroundColor(background, getContents());
}
public boolean isFocusControl() {
return getShell().getDisplay().getActiveShell() == getShell();
}
public void setFocus() {
getShell().forceFocus();
}
public void addFocusListener(FocusListener listener) {
getShell().addFocusListener(listener);
}
public void removeFocusListener(FocusListener listener) {
getShell().removeFocusListener(listener);
}
public void setSizeConstraints(int maxWidth, int maxHeight) {
// ignore
}
public void setLocation(Point location) {
/*
* If the location is persisted, it gets managed by PopupDialog - fine. Otherwise, the location is
* computed in Window#getInitialLocation, which will center it in the parent shell / main
* monitor, which is wrong for two reasons:
* - we want to center over the editor / subject control, not the parent shell
* - the center is computed via the initalSize, which may be also wrong since the size may
* have been updated since via min/max sizing of AbstractInformationControlManager.
* In that case, override the location with the one computed by the manager. Note that
* the call to constrainShellSize in PopupDialog.open will still ensure that the shell is
* entirely visible.
*/
if (!getPersistLocation() || getDialogSettings() == null)
getShell().setLocation(location);
}
public Point computeSizeHint() {
// return the shell's size - note that it already has the persisted size if persisting
// is enabled.
return getShell().getSize();
}
public void setVisible(boolean visible) {
if (visible) {
open();
} else {
saveDialogBounds(getShell());
getShell().setVisible(false);
}
}
public final void dispose() {
docProvider.disconnect(ei);
ei = null;
close();
}
@Override
public void setInput(Object input) {
CeylonParseController controller =
editor.getParseController();
IRegion region = editor.getSelection();
int offset = region.getOffset();
int length = region.getLength();
Tree.CompilationUnit rootNode =
controller.getLastCompilationUnit();
referencedNode =
getReferencedNode(findNode(rootNode,
controller.getTokens(),
offset, offset+length));
if (referencedNode==null) return;
IProject project = controller.getProject();
IPath path = getNodePath(referencedNode);
//CeylonParseController treats files with full paths subtly
//differently to files with relative paths, so make the
//path relative
IPath pathToCompare = path;
if (project!=null &&
project.getLocation().isPrefixOf(path)) {
pathToCompare =
path.makeRelativeTo(project.getLocation());
}
IDocument doc;
if (pathToCompare.equals(controller.getPath())) {
doc = controller.getDocument();
}
else {
ei = getEditorInput(referencedNode.getUnit());
if (ei == null) {
ei = getEditorInput(path);
}
try {
docProvider.connect(ei);
doc = docProvider.getDocument(ei);
}
catch (CoreException e) {
e.printStackTrace();
return;
}
}
viewer.setDocument(doc);
try {
IRegion firstLine =
doc.getLineInformationOfOffset(referencedNode.getStartIndex());
IRegion lastLine =
doc.getLineInformationOfOffset(referencedNode.getStopIndex());
viewer.setVisibleRegion(firstLine.getOffset(),
lastLine.getOffset()+lastLine.getLength()-firstLine.getOffset());
}
catch (BadLocationException e) {
e.printStackTrace();
}
parseController.initialize(path, project, null);
if (parseController.parseAndTypecheck(doc, 10,
new NullProgressMonitor(),
null) != null) {
if (referencedNode instanceof Tree.Declaration) {
Tree.Declaration declaration =
(Tree.Declaration) referencedNode;
Declaration model =
declaration.getDeclarationModel();
setTitleText("Peek Definition \u2014 " +
getLabelDescriptionFor(model));
}
}
}
@Override
public boolean restoresLocation() {
return getPersistLocation();
}
@Override
public boolean restoresSize() {
return getPersistSize();
}
@Override
public Rectangle getBounds() {
return getShell().getBounds();
}
@Override
public Rectangle computeTrim() {
return getShell().computeTrim(0, 0, 0, 0);
}
@Override
protected IDialogSettings getDialogSettings() {
String sectionName =
CeylonPlugin.PLUGIN_ID + ".PeekDefinition";
IDialogSettings dialogSettings =
CeylonPlugin.getInstance().getDialogSettings();
IDialogSettings settings =
dialogSettings.getSection(sectionName);
if (settings == null)
settings = dialogSettings.addNewSection(sectionName);
return settings;
}
}