blob: ce022e648c01126d22f872eff7d9c1be277b3a98 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2015 IBM Corporation 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.text.link.contentassist;
import java.util.Stack;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationExtension;
import org.eclipse.jface.text.contentassist.IContextInformationPresenter;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
/**
* This class is used to present context information to the user.
* If multiple contexts are valid at the current cursor location,
* a list is presented from which the user may choose one context.
* Once the user makes their choice, or if there was only a single
* possible context, the context information is shown in a tooltip like popup. <p>
* If the tooltip is visible and the user wants to see context information of
* a context embedded into the one for which context information is displayed,
* context information for the embedded context is shown. As soon as the
* cursor leaves the embedded context area, the context information for
* the embedding context is shown again.
*
* @see IContextInformation
* @see IContextInformationValidator
*/
class ContextInformationPopup2 implements IContentAssistListener2 {
/**
* Represents the state necessary for embedding contexts.
* @since 2.0
*/
static class ContextFrame {
public int fBeginOffset;
public int fOffset;
public int fVisibleOffset;
public IContextInformation fInformation;
public IContextInformationValidator fValidator;
public IContextInformationPresenter fPresenter;
}
private ITextViewer fViewer;
private ContentAssistant2 fContentAssistant;
private PopupCloser2 fPopupCloser= new PopupCloser2();
private Shell fContextSelectorShell;
private Table fContextSelectorTable;
private IContextInformation[] fContextSelectorInput;
private String fLineDelimiter= null;
private Shell fContextInfoPopup;
private StyledText fContextInfoText;
private TextPresentation fTextPresentation;
private Stack<ContextFrame> fContextFrameStack= new Stack<>();
/**
* Creates a new context information popup.
*
* @param contentAssistant the content assist for computing the context information
* @param viewer the viewer on top of which the context information is shown
*/
public ContextInformationPopup2(ContentAssistant2 contentAssistant, ITextViewer viewer) {
fContentAssistant= contentAssistant;
fViewer= viewer;
}
/**
* Shows all possible contexts for the given cursor position of the viewer.
*
* @param autoActivated <code>true</code> if auto activated
* @return a potential error message or <code>null</code> in case of no error
*/
public String showContextProposals(final boolean autoActivated) {
final StyledText styledText= fViewer.getTextWidget();
BusyIndicator.showWhile(styledText.getDisplay(), new Runnable() {
@Override
public void run() {
int position= fViewer.getSelectedRange().x;
IContextInformation[] contexts= computeContextInformation(position);
int count = (contexts == null ? 0 : contexts.length);
if (count == 1) {
// Show context information directly
internalShowContextInfo(contexts[0], position);
} else if (count > 0) {
// Precise context must be selected
if (fLineDelimiter == null)
fLineDelimiter= styledText.getLineDelimiter();
createContextSelector();
setContexts(contexts);
displayContextSelector();
hideContextInfoPopup();
}
}
});
return getErrorMessage();
}
/**
* Displays the given context information for the given offset.
*
* @param info the context information
* @param position the offset
* @since 2.0
*/
public void showContextInformation(final IContextInformation info, final int position) {
Control control= fViewer.getTextWidget();
BusyIndicator.showWhile(control.getDisplay(), new Runnable() {
@Override
public void run() {
internalShowContextInfo(info, position);
hideContextSelector();
}
});
}
/**
* Displays the given context information for the given offset.
*
* @param information the context information
* @param offset the offset
* @since 2.0
*/
private void internalShowContextInfo(IContextInformation information, int offset) {
IContextInformationValidator validator= fContentAssistant.getContextInformationValidator(fViewer, offset);
if (validator != null) {
ContextFrame current= new ContextFrame();
current.fInformation= information;
current.fBeginOffset= (information instanceof IContextInformationExtension) ? ((IContextInformationExtension) information).getContextInformationPosition() : offset;
if (current.fBeginOffset == -1) current.fBeginOffset= offset;
current.fOffset= offset;
current.fVisibleOffset= fViewer.getTextWidget().getSelectionRange().x - (offset - current.fBeginOffset);
current.fValidator= validator;
current.fPresenter= fContentAssistant.getContextInformationPresenter(fViewer, offset);
fContextFrameStack.push(current);
internalShowContextFrame(current, fContextFrameStack.size() == 1);
}
}
/**
* Shows the given context frame.
*
* @param frame the frane to display
* @param initial <code>true</code> if this is the first frame to be displayed
* @since 2.0
*/
private void internalShowContextFrame(ContextFrame frame, boolean initial) {
frame.fValidator.install(frame.fInformation, fViewer, frame.fOffset);
if (frame.fPresenter != null) {
if (fTextPresentation == null)
fTextPresentation= new TextPresentation();
frame.fPresenter.install(frame.fInformation, fViewer, frame.fBeginOffset);
frame.fPresenter.updatePresentation(frame.fOffset, fTextPresentation);
}
createContextInfoPopup();
fContextInfoText.setText(frame.fInformation.getInformationDisplayString());
if (fTextPresentation != null)
TextPresentation.applyTextPresentation(fTextPresentation, fContextInfoText);
resize();
if (initial) {
if (fContentAssistant.addContentAssistListener(this, ContentAssistant2.CONTEXT_INFO_POPUP)) {
fContentAssistant.addToLayout(this, fContextInfoPopup, ContentAssistant2.LayoutManager.LAYOUT_CONTEXT_INFO_POPUP, frame.fVisibleOffset);
fContextInfoPopup.setVisible(true);
}
} else {
fContentAssistant.layout(ContentAssistant2.LayoutManager.LAYOUT_CONTEXT_INFO_POPUP, frame.fVisibleOffset);
}
}
/**
* Computes all possible context information for the given offset.
*
* @param position the offset
* @return all possible context information for the given offset
* @since 2.0
*/
private IContextInformation[] computeContextInformation(int position) {
return fContentAssistant.computeContextInformation(fViewer, position);
}
/**
*Returns the error message generated while computing context information.
*
* @return the error message
*/
private String getErrorMessage() {
return fContentAssistant.getErrorMessage();
}
/**
* Creates the context information popup. This is the tooltip like overlay window.
*/
private void createContextInfoPopup() {
if (Helper2.okToUse(fContextInfoPopup))
return;
Control control= fViewer.getTextWidget();
Display display= control.getDisplay();
fContextInfoPopup= new Shell(control.getShell(), SWT.NO_TRIM | SWT.ON_TOP);
fContextInfoPopup.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
fContextInfoText= new StyledText(fContextInfoPopup, SWT.MULTI | SWT.READ_ONLY);
Color c= fContentAssistant.getContextInformationPopupBackground();
if (c == null)
c= display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
fContextInfoText.setBackground(c);
c= fContentAssistant.getContextInformationPopupForeground();
if (c == null)
c= display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
fContextInfoText.setForeground(c);
}
/**
* Resizes the context information popup.
* @since 2.0
*/
private void resize() {
Point size= fContextInfoText.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
size.x += 3;
fContextInfoText.setSize(size);
fContextInfoText.setLocation(1,1);
size.x += 2;
size.y += 2;
fContextInfoPopup.setSize(size);
}
/**
*Hides the context information popup.
*/
private void hideContextInfoPopup() {
if (Helper2.okToUse(fContextInfoPopup)) {
int size= fContextFrameStack.size();
if (size > 0) {
fContextFrameStack.pop();
-- size;
}
if (size > 0) {
ContextFrame current= fContextFrameStack.peek();
internalShowContextFrame(current, false);
} else {
fContentAssistant.removeContentAssistListener(this, ContentAssistant2.CONTEXT_INFO_POPUP);
fContextInfoPopup.setVisible(false);
fContextInfoPopup.dispose();
fContextInfoPopup= null;
if (fTextPresentation != null) {
fTextPresentation.clear();
fTextPresentation= null;
}
}
}
if (fContextInfoPopup == null)
fContentAssistant.contextInformationClosed();
}
/**
* Creates the context selector in case the user has the choice between multiple valid contexts
* at a given offset.
*/
private void createContextSelector() {
if (Helper2.okToUse(fContextSelectorShell))
return;
Control control= fViewer.getTextWidget();
fContextSelectorShell= new Shell(control.getShell(), SWT.NO_TRIM | SWT.ON_TOP);
GridLayout layout= new GridLayout();
layout.marginWidth= 0;
layout.marginHeight= 0;
fContextSelectorShell.setLayout(layout);
fContextSelectorShell.setBackground(control.getDisplay().getSystemColor(SWT.COLOR_BLACK));
fContextSelectorTable= new Table(fContextSelectorShell, SWT.H_SCROLL | SWT.V_SCROLL);
fContextSelectorTable.setLocation(1, 1);
GridData gd= new GridData(GridData.FILL_BOTH);
gd.heightHint= fContextSelectorTable.getItemHeight() * 10;
gd.widthHint= 300;
fContextSelectorTable.setLayoutData(gd);
fContextSelectorShell.pack(true);
Color c= fContentAssistant.getContextSelectorBackground();
if (c == null)
c= control.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
fContextSelectorTable.setBackground(c);
c= fContentAssistant.getContextSelectorForeground();
if (c == null)
c= control.getDisplay().getSystemColor(SWT.COLOR_INFO_FOREGROUND);
fContextSelectorTable.setForeground(c);
fContextSelectorTable.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent e) {
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
insertSelectedContext();
hideContextSelector();
}
});
fPopupCloser.install(fContentAssistant, fContextSelectorTable);
fContextSelectorTable.setHeaderVisible(false);
fContentAssistant.addToLayout(this, fContextSelectorShell, ContentAssistant2.LayoutManager.LAYOUT_CONTEXT_SELECTOR, fContentAssistant.getSelectionOffset());
}
/**
* Causes the context information of the context selected in the context selector
* to be displayed in the context information popup.
*/
private void insertSelectedContext() {
int i= fContextSelectorTable.getSelectionIndex();
if (i < 0 || i >= fContextSelectorInput.length)
return;
int position= fViewer.getSelectedRange().x;
internalShowContextInfo(fContextSelectorInput[i], position);
}
/**
* Sets the contexts in the context selector to the given set.
*
* @param contexts the possible contexts
*/
private void setContexts(IContextInformation[] contexts) {
if (Helper2.okToUse(fContextSelectorTable)) {
fContextSelectorInput= contexts;
fContextSelectorTable.setRedraw(false);
fContextSelectorTable.removeAll();
TableItem item;
IContextInformation t;
for (int i= 0; i < contexts.length; i++) {
t= contexts[i];
item= new TableItem(fContextSelectorTable, SWT.NULL);
if (t.getImage() != null)
item.setImage(t.getImage());
item.setText(t.getContextDisplayString());
}
fContextSelectorTable.select(0);
fContextSelectorTable.setRedraw(true);
}
}
/**
* Displays the context selector.
*/
private void displayContextSelector() {
if (fContentAssistant.addContentAssistListener(this, ContentAssistant2.CONTEXT_SELECTOR))
fContextSelectorShell.setVisible(true);
}
/**
* Hodes the context selector.
*/
private void hideContextSelector() {
if (Helper2.okToUse(fContextSelectorShell)) {
fContentAssistant.removeContentAssistListener(this, ContentAssistant2.CONTEXT_SELECTOR);
fPopupCloser.uninstall();
fContextSelectorShell.setVisible(false);
fContextSelectorShell.dispose();
fContextSelectorShell= null;
}
if (!Helper2.okToUse(fContextInfoPopup))
fContentAssistant.contextInformationClosed();
}
/**
*Returns whether the context selector has the focus.
*
* @return <code>true</code> if teh context selector has the focus
*/
public boolean hasFocus() {
if (Helper2.okToUse(fContextSelectorShell))
return (fContextSelectorShell.isFocusControl() || fContextSelectorTable.isFocusControl());
return false;
}
/**
* Hides context selector and context information popup.
*/
public void hide() {
hideContextSelector();
hideContextInfoPopup();
}
/**
* Returns whether this context information popup is active. I.e., either
* a context selector or context information is displayed.
*
* @return <code>true</code> if the context selector is active
*/
public boolean isActive() {
return (Helper2.okToUse(fContextInfoPopup) || Helper2.okToUse(fContextSelectorShell));
}
@Override
public boolean verifyKey(VerifyEvent e) {
if (Helper2.okToUse(fContextSelectorShell))
return contextSelectorKeyPressed(e);
if (Helper2.okToUse(fContextInfoPopup))
return contextInfoPopupKeyPressed(e);
return true;
}
/**
* Processes a key stroke in the context selector.
*
* @param e the verify event describing the key stroke
* @return <code>true</code> if processing can be stopped
*/
private boolean contextSelectorKeyPressed(VerifyEvent e) {
char key= e.character;
if (key == 0) {
int change;
int visibleRows= (fContextSelectorTable.getSize().y / fContextSelectorTable.getItemHeight()) - 1;
int selection= fContextSelectorTable.getSelectionIndex();
switch (e.keyCode) {
case SWT.ARROW_UP:
change= (fContextSelectorTable.getSelectionIndex() > 0 ? -1 : 0);
break;
case SWT.ARROW_DOWN:
change= (fContextSelectorTable.getSelectionIndex() < fContextSelectorTable.getItemCount() - 1 ? 1 : 0);
break;
case SWT.PAGE_DOWN :
change= visibleRows;
if (selection + change >= fContextSelectorTable.getItemCount())
change= fContextSelectorTable.getItemCount() - selection;
break;
case SWT.PAGE_UP :
change= -visibleRows;
if (selection + change < 0)
change= -selection;
break;
case SWT.HOME :
change= -selection;
break;
case SWT.END :
change= fContextSelectorTable.getItemCount() - selection;
break;
default:
if (e.keyCode != SWT.MOD1 && e.keyCode != SWT.MOD2 && e.keyCode != SWT.MOD3 && e.keyCode != SWT.MOD4)
hideContextSelector();
return true;
}
fContextSelectorTable.setSelection(selection + change);
fContextSelectorTable.showSelection();
e.doit= false;
return false;
} else if ('\t' == key) {
// switch focus to selector shell
e.doit= false;
fContextSelectorShell.setFocus();
return false;
} else if (key == SWT.ESC) {
e.doit= false;
hideContextSelector();
}
return true;
}
/**
* Processes a key stroke while the info popup is up.
*
* @param e the verify event describing the key stroke
* @return <code>true</code> if processing can be stopped
*/
private boolean contextInfoPopupKeyPressed(KeyEvent e) {
char key= e.character;
if (key == 0) {
switch (e.keyCode) {
case SWT.ARROW_LEFT:
case SWT.ARROW_RIGHT:
case SWT.ARROW_UP:
case SWT.ARROW_DOWN:
validateContextInformation();
break;
default:
if (e.keyCode != SWT.MOD1 && e.keyCode != SWT.MOD2 && e.keyCode != SWT.MOD3 && e.keyCode != SWT.MOD4)
hideContextInfoPopup();
break;
}
} else if (key == SWT.ESC) {
e.doit= false;
hideContextInfoPopup();
} else {
validateContextInformation();
}
return true;
}
@Override
public void processEvent(VerifyEvent event) {
if (Helper2.okToUse(fContextSelectorShell))
contextSelectorProcessEvent(event);
if (Helper2.okToUse(fContextInfoPopup))
contextInfoPopupProcessEvent(event);
}
/**
* Processes a key stroke in the context selector.
*
* @param e the verify event describing the key stroke
*/
private void contextSelectorProcessEvent(VerifyEvent e) {
if (e.start == e.end && e.text != null && e.text.equals(fLineDelimiter)) {
e.doit= false;
insertSelectedContext();
}
hideContextSelector();
}
/**
* Processes a key stroke while the info popup is up.
*
* @param e the verify event describing the key stroke
*/
private void contextInfoPopupProcessEvent(VerifyEvent e) {
if (e.start != e.end && (e.text == null || e.text.length() == 0))
validateContextInformation();
}
/**
* Validates the context information for the viewer's actual cursor position.
*/
private void validateContextInformation() {
/*
* Post the code in the event queue in order to ensure that the
* action described by this verify key event has already beed executed.
* Otherwise, we'd validate the context information based on the
* pre-key-stroke state.
*/
fContextInfoPopup.getDisplay().asyncExec(new Runnable() {
private ContextFrame fFrame= fContextFrameStack.peek();
@Override
public void run() {
if (Helper2.okToUse(fContextInfoPopup) && fFrame == fContextFrameStack.peek()) {
int offset= fViewer.getSelectedRange().x;
if (fFrame.fValidator == null || !fFrame.fValidator.isContextInformationValid(offset)) {
hideContextInfoPopup();
} else if (fFrame.fPresenter != null && fFrame.fPresenter.updatePresentation(offset, fTextPresentation)) {
TextPresentation.applyTextPresentation(fTextPresentation, fContextInfoText);
resize();
}
}
}
});
}
}