/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.ui.texteditor;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.TextViewerUndoManager;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Layout;
import org.xmind.ui.viewers.ICompositeProvider;
import org.xmind.ui.viewers.SWTUtils;
import org.xmind.ui.viewers.SameCompositeProvider;
/**
* A lightweight viewer observing text change using a Text as widget.
*
* @author Frank Shaka
*/
public class FloatingTextEditor extends Viewer implements ITextOperationTarget {
private static int DEFAULT_STYLE = SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL
| SWT.H_SCROLL;
private class TextViewerHooker implements ISelectionChangedListener,
VerifyKeyListener, TraverseListener {
public void selectionChanged(SelectionChangedEvent event) {
fireSelectionChanged(new SelectionChangedEvent(
FloatingTextEditor.this, event.getSelection()));
}
public void verifyKey(VerifyEvent event) {
handleVerifyKey(event);
}
/*
* (non-Javadoc)
* @see
* org.eclipse.swt.events.TraverseListener#keyTraversed(org.eclipse.
* swt.events.TraverseEvent)
*/
public void keyTraversed(TraverseEvent e) {
handleTraverseKey(e);
}
public void hook(ITextViewer viewer) {
if (viewer instanceof ISelectionProvider) {
((ISelectionProvider) viewer).addSelectionChangedListener(this);
}
if (viewer instanceof IPostSelectionProvider) {
((IPostSelectionProvider) viewer)
.addPostSelectionChangedListener(this);
}
if (viewer instanceof ITextViewerExtension) {
((ITextViewerExtension) viewer).prependVerifyKeyListener(this);
}
viewer.getTextWidget().addTraverseListener(this);
}
public void unhook(ITextViewer viewer) {
viewer.getTextWidget().removeTraverseListener(this);
if (viewer instanceof ITextViewerExtension) {
((ITextViewerExtension) viewer).removeVerifyKeyListener(this);
}
if (viewer instanceof IPostSelectionProvider) {
((IPostSelectionProvider) viewer)
.removePostSelectionChangedListener(this);
}
if (viewer instanceof ISelectionProvider) {
((ISelectionProvider) viewer)
.removeSelectionChangedListener(this);
}
}
}
private int style = DEFAULT_STYLE;
private ICompositeProvider compositeProvider;
private Composite control = null;
private IDocument document = null;
private ITextViewer textViewer = null;
private IDocumentListener documentListener = null;
private List<IFloatingTextEditorListener> textEditorListeners = null;
private List<VerifyKeyListener> verifyKeyListeners = null;
private TextViewerHooker textViewerHooker = null;
private boolean closing = false;
private Point initialLocation = null;
private Point initialSize = null;
public FloatingTextEditor(Composite parent) {
this(new SameCompositeProvider(parent), DEFAULT_STYLE);
}
public FloatingTextEditor(Composite parent, int style) {
this(new SameCompositeProvider(parent), style);
}
public FloatingTextEditor(ICompositeProvider compositeProvider) {
this(compositeProvider, DEFAULT_STYLE);
}
public FloatingTextEditor(ICompositeProvider compositeProvider, int style) {
Assert.isNotNull(compositeProvider);
this.compositeProvider = compositeProvider;
this.style = style;
}
protected Composite getParentComposite() {
return compositeProvider.getParent();
}
/**
* Sets the style of the editor. Has no effect after the control has been
* created.
* <dl>
* <dt><b>Styles<b></dt>
* <dd>NO_FOCUS, BORDER, SINGLE, MULTI, READ_ONLY, V_SCROLL, H_SCROLL,
* WRAP</dd>
* </dl>
*
* @param style
*/
protected void setEditorStyle(int style) {
this.style = style;
}
protected int getEditorStyle() {
return style;
}
public boolean open() {
return open(true);
}
public boolean open(boolean withFocus) {
if (!isClosed())
return true;
TextEvent e = createTextEvent();
fireEditingAboutToStart(e);
if (e.isCanceled())
return false;
Composite parent = getParentComposite();
if (parent == null)
return false;
if (isClosed())
createControl(parent, getEditorStyle());
if (isClosed())
return false;
control.setVisible(true);
if (withFocus) {
setFocus();
}
fireEditingStarted(createTextEvent());
return true;
}
public void setFocus() {
if (textViewer != null && !textViewer.getTextWidget().isDisposed()) {
textViewer.getTextWidget().setFocus();
} else if (control != null && !control.isDisposed()) {
control.setFocus();
}
}
public boolean close() {
return close(false);
}
public boolean close(boolean finish) {
if (isClosed() || closing)
return true;
closing = true;
TextEvent e = createTextEvent();
if (finish) {
fireEditingAboutToFinish(e);
} else {
fireEditingAboutToCancel(e);
}
if (e.isCanceled()) {
closing = false;
return false;
}
hardClose(finish);
if (finish) {
fireEditingFinished(createTextEvent());
} else {
fireEditingCanceled(createTextEvent());
}
closing = false;
return true;
}
protected void hardClose(boolean finish) {
if (textViewer != null) {
unhookTextViewer(textViewer);
}
if (document != null) {
unhookDocument(document);
}
if (!finish)
hardCancel();
Composite parent = control.getParent();
boolean wasFocused = isFocused();
control.dispose();
if (wasFocused
&& parent.getShell() == parent.getDisplay().getActiveShell()) {
parent.setFocus();
}
}
protected boolean isFocused() {
return textViewer != null && !textViewer.getTextWidget().isDisposed()
&& textViewer.getTextWidget().isFocusControl();
}
protected void hardCancel() {
if (document != null) {
while (canDoOperation(UNDO)) {
doOperation(UNDO);
}
}
}
protected TextEvent createTextEvent() {
return new TextEvent(this, getTextContents());
}
public String getTextContents() {
return document == null ? null : document.get();
}
private void createControl(Composite parent, int style) {
boolean border = (style & SWT.BORDER) != 0;
style &= ~(SWT.BORDER | SWT.NO_FOCUS);
control = createContainer(parent, border);
textViewer = createTextViewer(control, style);
configureContainer(control);
hookContainer(control);
configureTextViewer(textViewer);
hookTextViewer(textViewer);
if (document != null) {
hookDocument(document);
}
}
private Composite createContainer(Composite parent, boolean border) {
Composite composite = new Composite(parent, SWT.NO_FOCUS);
composite.setBackground(
parent.getDisplay().getSystemColor(SWT.COLOR_GRAY));
GridLayout layout = new GridLayout();
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
if (border) {
layout.marginWidth = 1;
layout.marginHeight = 1;
}
composite.setLayout(layout);
return composite;
}
protected void configureContainer(Composite container) {
Point size = getInitialSize();
Point position = getInitialPosition(size);
container.setBounds(position.x, position.y, size.x, size.y);
}
protected Point getInitialSize() {
if (initialSize != null)
return initialSize;
return new Point(100, 20);
}
protected Point getInitialPosition(Point size) {
if (initialLocation != null)
return initialLocation;
return new Point(0, 0);
}
protected void hookContainer(Composite container) {
container.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
handleDispose(e);
}
});
}
protected void handleDispose(DisposeEvent e) {
if (document != null) {
unhookDocument(document);
}
if (textViewer != null) {
unhookTextViewer(textViewer);
textViewer = null;
}
control = null;
}
protected ITextViewer createTextViewer(Composite parent, int style) {
TextViewer viewer = new TextViewer(parent, style) {
protected int getEmptySelectionChangedEventDelay() {
return 300;
}
@Override
protected StyledText createTextWidget(Composite parent,
int styles) {
StyledText styledText = new StyledText(parent, styles) {
@Override
public void invokeAction(int action) {
super.invokeAction(action);
//TODO force send null event , when floating text is editing state,
// remove out listeners of chain.
switch (action) {
case ST.DELETE_NEXT:
Event event = new Event();
notifyListeners(SWT.Modify, event);
return;
}
}
};
styledText
.setLeftMargin(Math.max(styledText.getLeftMargin(), 2));
return styledText;
}
};
viewer.getControl().setLayoutData(
new GridData(GridData.FILL, GridData.FILL, true, true));
return viewer;
}
protected void configureTextViewer(ITextViewer viewer) {
if (getDocument() != null) {
viewer.setDocument(getDocument());
}
viewer.setUndoManager(new TextViewerUndoManager(20));
viewer.activatePlugins();
}
protected void hookTextViewer(ITextViewer viewer) {
if (textViewerHooker == null) {
textViewerHooker = new TextViewerHooker();
}
textViewerHooker.hook(viewer);
}
protected void unhookTextViewer(ITextViewer viewer) {
if (textViewerHooker != null) {
textViewerHooker.unhook(viewer);
textViewerHooker = null;
}
}
protected void handleVerifyKey(VerifyEvent event) {
fireVerifyKey(event);
if (!event.doit)
return;
int stateMask = event.stateMask;
int keyCode = event.keyCode;
if (SWTUtils.matchKey(stateMask, keyCode, SWT.MOD2, SWT.CR)) {
if ((getEditorStyle() & SWT.MULTI) == 0) {
event.doit = false;
}
} else if (SWTUtils.matchKey(stateMask, keyCode, SWT.MOD1, 'z')) {
event.doit = false;
if (canDoOperation(UNDO)) {
doOperation(UNDO);
}
} else if (SWTUtils.matchKey(stateMask, keyCode, SWT.MOD1, 'y')) {
event.doit = false;
if (canDoOperation(REDO)) {
doOperation(REDO);
}
}
}
protected void handleTraverseKey(TraverseEvent event) {
if (event.detail == SWT.TRAVERSE_ESCAPE) {
fireTraverseKey(event, event.stateMask, SWT.ESC);
if (!event.doit)
return;
event.doit = false;
cancelEditing();
} else if (event.detail == SWT.TRAVERSE_RETURN) {
fireTraverseKey(event, event.stateMask, SWT.CR);
if (!event.doit)
return;
if (event.stateMask == 0) {
event.doit = false;
finishEditing();
} else if ((event.stateMask & SWT.MOD2) != 0) {
if ((getEditorStyle() & SWT.MULTI) == 0) {
event.doit = false;
}
}
}
}
protected void replaceText(int offset, int length, String text) {
if (document == null)
return;
try {
document.replace(offset, length, text);
} catch (BadLocationException e) {
}
}
protected void finishEditing() {
close(true);
}
protected void cancelEditing() {
close(false);
}
protected void hookDocument(IDocument document) {
if (documentListener == null) {
documentListener = new IDocumentListener() {
public void documentChanged(DocumentEvent event) {
fireTextChanged(createTextEvent());
}
public void documentAboutToBeChanged(DocumentEvent event) {
fireTextAboutToChange(createTextEvent());
}
};
}
document.addDocumentListener(documentListener);
}
protected void unhookDocument(IDocument document) {
if (document != null && documentListener != null) {
document.removeDocumentListener(documentListener);
documentListener = null;
}
}
public boolean isClosed() {
return control == null || control.isDisposed();
}
public Control getControl() {
return control;
}
public Object getInput() {
return getDocument();
}
protected IDocument getDocument() {
return document;
}
public ISelection getSelection() {
if (textViewer instanceof ISelectionProvider)
return ((ISelectionProvider) textViewer).getSelection();
return null;
}
public void refresh() {
if (textViewer instanceof Viewer) {
((Viewer) textViewer).refresh();
}
}
public void setInput(Object input) {
if (!(input instanceof IDocument))
return;
IDocument newDocument = (IDocument) input;
IDocument oldDocument = this.document;
if (newDocument == oldDocument
|| (newDocument != null && newDocument.equals(oldDocument)))
return;
this.document = newDocument;
documentChanged(oldDocument, newDocument);
}
protected void documentChanged(IDocument oldDocument,
IDocument newDocument) {
if (oldDocument != null) {
unhookDocument(oldDocument);
}
if (textViewer != null) {
textViewer.setDocument(newDocument);
if (newDocument != null) {
hookDocument(newDocument);
}
}
inputChanged(newDocument, oldDocument);
}
public void setSelection(ISelection selection) {
if (textViewer instanceof ISelectionProvider)
((ISelectionProvider) textViewer).setSelection(selection);
}
public void setSelection(ISelection selection, boolean reveal) {
if (textViewer instanceof Viewer)
((Viewer) textViewer).setSelection(selection, reveal);
}
public ITextViewer getTextViewer() {
return textViewer;
}
public void doOperation(int operation) {
if (textViewer instanceof ITextOperationTarget) {
((ITextOperationTarget) textViewer).doOperation(operation);
}
}
public boolean canDoOperation(int operation) {
if (textViewer instanceof ITextOperationTarget) {
return ((ITextOperationTarget) textViewer)
.canDoOperation(operation);
}
return false;
}
public void addFloatingTextEditorListener(
IFloatingTextEditorListener listener) {
if (textEditorListeners == null)
textEditorListeners = new ArrayList<IFloatingTextEditorListener>();
textEditorListeners.add(listener);
}
public void removeFloatingTextEditorListener(
IFloatingTextEditorListener listener) {
if (textEditorListeners == null)
return;
textEditorListeners.remove(listener);
}
protected void fireEditingAboutToStart(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingAboutToStart(e);
}
}
protected void fireEditingStarted(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingStarted(e);
}
}
protected void fireEditingAboutToCancel(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingAboutToCancel(e);
}
}
protected void fireEditingCanceled(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingCanceled(e);
}
}
protected void fireEditingAboutToFinish(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingAboutToFinish(e);
}
}
protected void fireEditingFinished(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).editingFinished(e);
}
}
protected void fireTextAboutToChange(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).textAboutToChange(e);
}
}
protected void fireTextChanged(TextEvent e) {
if (textEditorListeners == null)
return;
for (Object l : textEditorListeners.toArray()) {
((IFloatingTextEditorListener) l).textChanged(e);
}
}
public void addVerifyKeyListener(VerifyKeyListener listener) {
if (verifyKeyListeners == null)
verifyKeyListeners = new ArrayList<VerifyKeyListener>();
verifyKeyListeners.add(listener);
}
public void removeVerifyKeyListener(VerifyKeyListener listener) {
if (verifyKeyListeners == null)
return;
verifyKeyListeners.remove(listener);
}
protected void fireTraverseKey(TraverseEvent te, int simState,
int simKeyCode) {
StyledText widget = (StyledText) te.widget;
Point selection = widget.getSelection();
Event e = new Event();
e.character = te.character;
e.data = te.data;
e.display = te.display;
e.doit = true;
e.end = selection.y;
e.keyCode = te.keyCode;
e.keyLocation = te.keyLocation;
e.start = selection.x;
e.stateMask = te.stateMask;
e.text = widget.getText();
e.time = te.time;
e.widget = te.widget;
VerifyEvent ve = new VerifyEvent(e);
try {
handleVerifyKey(ve);
} finally {
te.doit = ve.doit;
}
}
protected void fireVerifyKey(VerifyEvent e) {
if (verifyKeyListeners == null)
return;
for (Object l : verifyKeyListeners.toArray()) {
((VerifyKeyListener) l).verifyKey(e);
}
}
public Rectangle computeTrim(int x, int y, int width, int height) {
if (isClosed())
return null;
Rectangle trim = textViewer.getTextWidget().computeTrim(x, y, width,
height);
trim = control.computeTrim(trim.x, trim.y, trim.width, trim.height);
Layout layout = control.getLayout();
if (layout instanceof GridLayout) {
GridLayout gl = (GridLayout) layout;
trim.x -= gl.marginWidth + gl.marginLeft;
trim.y -= gl.marginHeight + gl.marginTop;
trim.width += gl.marginWidth * 2 + gl.marginLeft + gl.marginRight;
trim.height += gl.marginHeight * 2 + gl.marginTop + gl.marginBottom;
}
return trim;
}
public void replaceText(String text) {
replaceText(text, false);
}
public void replaceText(String text, boolean select) {
if (isClosed() || document == null)
return;
Point range = textViewer.getSelectedRange();
try {
document.replace(range.x, range.y, text);
} catch (BadLocationException e) {
}
if (select) {
textViewer.setSelectedRange(range.x, text.length());
} else {
textViewer.setSelectedRange(range.x + text.length(), 0);
}
}
public void setInitialLocation(Point initialLocation) {
this.initialLocation = initialLocation;
}
public void setInitialSize(Point initialSize) {
this.initialSize = initialSize;
}
}