/*******************************************************************************
* Copyright (c) 2007, 2012 Association for Decentralized Information Management
* in Industry THTH ry.
* 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:
* VTT Technical Research Centre of Finland - initial API and implementation
*******************************************************************************/
package org.simantics.browsing.ui.swt.widgets;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.State;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
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.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.simantics.browsing.ui.swt.widgets.impl.ITrackedColorProvider;
import org.simantics.browsing.ui.swt.widgets.impl.ReadFactory;
import org.simantics.browsing.ui.swt.widgets.impl.TextModifyListener;
import org.simantics.browsing.ui.swt.widgets.impl.TrackedModifyEvent;
import org.simantics.browsing.ui.swt.widgets.impl.Widget;
import org.simantics.browsing.ui.swt.widgets.impl.WidgetSupport;
import org.simantics.db.management.ISessionContext;
import org.simantics.db.procedure.Listener;
import org.simantics.ui.states.TrackedTextState;
/**
* This is a TrackedTest SWT Text-widget 'decorator'.
*
* The widget has 2 main states: editing and inactive.
*
* It implements the necessary listeners to achieve the text widget behaviour
* needed by Simantics. User notification about modifications is provided via
* {@link TextModifyListener}.
*
* Examples:
*
*
* // #1: create new Text internally, use TrackedModifylistener
* TrackedText trackedText = new TrackedText(parentComposite, style);
* trackedText.addModifyListener(new TrackedModifyListener() {
* public void modifyText(TrackedModifyEvent e) {
* // text was modified, do something.
* }
* });
*
* // #2: create new Text internally, define the colors for text states.
* TrackedText trackedText = new TrackedText(text, <instance of ITrackedColorProvider>);
*
*
* @author Tuukka Lehtonen
*/
public class TrackedText implements Widget {
public static final String ID = "TRACKED_TEXT";
private static final int EDITING = 1 << 0;
private static final int MODIFIED_DURING_EDITING = 1 << 1;
/**
* Used to tell whether or not a mouseDown has occurred after a focusGained
* event to be able to select the whole text field when it is pressed for
* the first time while the widget holds focus.
*/
private static final int MOUSE_DOWN_FIRST_TIME = 1 << 2;
private static final int MOUSE_INSIDE_CONTROL = 1 << 3;
private int state;
private int caretPositionBeforeEdit;
private String textBeforeEdit;
private final Display display;
private final Text text;
private CompositeListener listener;
private ListenerList modifyListeners;
private IInputValidator validator;
private ITrackedColorProvider colorProvider;
private final ResourceManager resourceManager;
private ReadFactory, String> textFactory;
private boolean moveCaretAfterEdit = true;
private boolean selectAllOnStartEdit = true;
private final CopyOnWriteArrayList> validationListeners = new CopyOnWriteArrayList<>();
// UNDO REDO HANDLER
private static final int MAX_STACK_SIZE = 25;
private List undoStack = new LinkedList();
private List redoStack = new LinkedList();
public void setTextFactory(ReadFactory, String> textFactory) {
this.textFactory = textFactory;
}
public void setFont(Font font) {
text.setFont(font);
}
public void setMoveCaretAfterEdit(boolean value) {
this.moveCaretAfterEdit = value;
}
@Override
public void setInput(ISessionContext context, Object input) {
if (modifyListeners != null) {
for (Object o : modifyListeners.getListeners()) {
if(o instanceof Widget) {
((Widget) o).setInput(context, input);
}
}
}
if(textFactory != null) {
textFactory.listen(context, input, new Listener() {
@Override
public void exception(final Throwable t) {
display.asyncExec(new Runnable() {
@Override
public void run() {
if(isDisposed()) return;
// System.out.println("Button received new text: " + text);
text.setText(t.toString());
}
});
}
@Override
public void execute(final String string) {
if(text.isDisposed()) return;
display.asyncExec(new Runnable() {
@Override
public void run() {
if(isDisposed()) return;
text.setText(string == null ? "" : string);
// text.getParent().layout();
// text.getParent().getParent().layout();
}
});
}
@Override
public boolean isDisposed() {
return text.isDisposed();
}
});
}
}
/**
* A composite of many UI listeners for creating the functionality of this
* class.
*/
private class CompositeListener
implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,
MouseListener, FocusListener
{
// Keyboard/editing events come in the following order:
// 1. keyPressed
// 2. verifyText
// 3. modifyText
// 4. keyReleased
@Override
public void modifyText(ModifyEvent e) {
//System.out.println("modifyText: " + e);
setModified(true);
String valid = isTextValid();
if (valid != null) {
setBackground(colorProvider.getInvalidBackground());
} else {
if (isEditing())
setBackground(colorProvider.getEditingBackground());
else
setBackground(colorProvider.getInactiveBackground());
}
}
@Override
public void widgetDisposed(DisposeEvent e) {
getWidget().removeModifyListener(this);
}
private boolean isMultiLine() {
return (text.getStyle() & SWT.MULTI) != 0;
}
private boolean hasMultiLineCommitModifier(KeyEvent e) {
return (e.stateMask & SWT.CTRL) != 0;
}
@Override
public void keyPressed(KeyEvent e) {
//System.out.println("keyPressed: " + e);
if (!isEditing()) {
// ESC, ENTER & keypad ENTER must not start editing
if (e.keyCode == SWT.ESC)
return;
if (!isMultiLine()) {
if (e.keyCode == SWT.F2 || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
startEdit(selectAllOnStartEdit);
} else if (e.character != '\0') {
startEdit(false);
}
} else {
// In multi-line mode, TAB must not start editing!
if (e.keyCode == SWT.F2) {
startEdit(selectAllOnStartEdit);
} else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
if (hasMultiLineCommitModifier(e)) {
e.doit = false;
} else {
startEdit(false);
}
} else if (e.keyCode == SWT.TAB) {
text.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT);
e.doit = false;
} else if (e.character != '\0') {
startEdit(false);
}
}
} else {
// ESC reverts any changes made during this edit
if (e.keyCode == SWT.ESC) {
revertEdit();
}
if (!isMultiLine()) {
if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
applyEdit();
}
} else {
if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
if (hasMultiLineCommitModifier(e)) {
applyEdit();
e.doit = false;
}
}
}
}
}
@Override
public void keyReleased(KeyEvent e) {
//System.out.println("keyReleased: " + e);
}
@Override
public void mouseEnter(MouseEvent e) {
//System.out.println("mouseEnter");
if (!isEditing()) {
setBackground(colorProvider.getHoverBackground());
}
setMouseInsideControl(true);
}
@Override
public void mouseExit(MouseEvent e) {
//System.out.println("mouseExit");
if (!isEditing()) {
setBackground(colorProvider.getInactiveBackground());
}
setMouseInsideControl(false);
}
@Override
public void mouseHover(MouseEvent e) {
//System.out.println("mouseHover");
setMouseInsideControl(true);
}
@Override
public void mouseDoubleClick(MouseEvent e) {
//System.out.println("mouseDoubleClick: " + e);
if (e.button == 1) {
getWidget().selectAll();
}
}
@Override
public void mouseDown(MouseEvent e) {
//System.out.println("mouseDown: " + e);
if (!isEditing()) {
// In reality we should never get here, since focusGained
// always comes before mouseDown, but let's keep this
// fallback just to be safe.
if (e.button == 1) {
startEdit(selectAllOnStartEdit);
}
} else {
if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {
if (!isMultiLine()) {
// This is useless for multi-line texts
getWidget().selectAll();
}
state &= ~MOUSE_DOWN_FIRST_TIME;
}
}
}
@Override
public void mouseUp(MouseEvent e) {
}
@Override
public void focusGained(FocusEvent e) {
//System.out.println("focusGained");
if (!isEditing()) {
if (!isMultiLine()) {
// Always start edit on single line texts when focus is gained
startEdit(selectAllOnStartEdit);
}
}
}
@Override
public void focusLost(FocusEvent e) {
//System.out.println("focusLost");
if (isEditing()) {
applyEdit();
}
}
}
public TrackedText(Composite parent, WidgetSupport support, int style) {
this.state = 0;
this.text = new Text(parent, style);
text.setData(ID, this);
this.display = text.getDisplay();
this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);
this.colorProvider = new DefaultColorProvider(resourceManager);
if (support!=null) support.register(this);
initialize();
createUndoRedoHandler();
}
private void createUndoRedoHandler() {
text.addModifyListener(new ModifyListener() {
private int eventTimeOut = 1000;
private long lastEventTimeStamp = 0;
@Override
public void modifyText(ModifyEvent event) {
String newText = text.getText().trim();
if (event.time - lastEventTimeStamp > eventTimeOut || newText.endsWith(" ")) {
if (newText != null && newText.length() > 0) {
if (undoStack.size() == MAX_STACK_SIZE) {
undoStack.remove(undoStack.size() - 1);
}
addToUndoStack(newText);
}
}
lastEventTimeStamp = (event.time & 0xFFFFFFFFL);
}
});
text.addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
Command command = service.getCommand( TrackedTextState.COMMAND_ID );
State state = command.getState( TrackedTextState.STATE_ID );
state.setValue(true);
}
@Override
public void focusGained(FocusEvent e) {
addToUndoStack(text.getText());
ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
Command command = service.getCommand( TrackedTextState.COMMAND_ID );
State state = command.getState( TrackedTextState.STATE_ID );
state.setValue(false);
}
});
}
public ResourceManager getResourceManager() {
return resourceManager;
}
/**
* Common initialization. Assumes that text is already created.
*/
private void initialize() {
Assert.isNotNull(text);
text.setBackground(colorProvider.getInactiveBackground());
text.setDoubleClickEnabled(false);
listener = new CompositeListener();
text.addModifyListener(listener);
text.addDisposeListener(listener);
text.addKeyListener(listener);
text.addMouseTrackListener(listener);
text.addMouseListener(listener);
text.addFocusListener(listener);
}
public void startEdit(boolean selectAll) {
if (isEditing()) {
// Print some debug incase we end are in an invalid state
System.out.println("TrackedText: BUG: startEdit called when in editing state");
}
// System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);
// Backup text-field data for reverting purposes
caretPositionBeforeEdit = text.getCaretPosition();
textBeforeEdit = text.getText();
// Signal editing state
setBackground(colorProvider.getEditingBackground());
if (selectAll) {
text.selectAll();
}
state |= EDITING | MOUSE_DOWN_FIRST_TIME;
}
private void applyEdit() {
try {
if (isTextValid() != null) {
text.setText(textBeforeEdit);
} else if (isModified() && !text.getText().equals(textBeforeEdit)) {
//System.out.println("apply");
if (modifyListeners != null) {
TrackedModifyEvent event = new TrackedModifyEvent(text, text.getText());
for (Object o : modifyListeners.getListeners()) {
((TextModifyListener) o).modifyText(event);
}
moveCursorToEnd();
}
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
endEdit();
}
}
private void endEdit() {
if (text.isDisposed())
return;
if (!isEditing()) {
// Print some debug incase we end are in an invalid state
//ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state"));
//System.out.println();
}
setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
// System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection());
// Always move the caret to the end of the string
if(moveCaretAfterEdit)
text.setSelection(text.getCharCount());
state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);
setModified(false);
}
private void revertEdit() {
if (!isEditing()) {
// Print some debug incase we end are in an invalid state
//ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state"));
System.out.println("BUG: revertEdit called when not in editing state");
}
text.setText(textBeforeEdit);
text.setSelection(caretPositionBeforeEdit);
setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);
setModified(false);
}
private boolean isEditing() {
return (state & EDITING) != 0;
}
private void setModified(boolean modified) {
if (modified) {
state |= MODIFIED_DURING_EDITING;
} else {
state &= ~MODIFIED_DURING_EDITING;
}
}
private boolean isMouseInsideControl() {
return (state & MOUSE_INSIDE_CONTROL) != 0;
}
private void setMouseInsideControl(boolean inside) {
if (inside)
state |= MOUSE_INSIDE_CONTROL;
else
state &= ~MOUSE_INSIDE_CONTROL;
}
private boolean isModified() {
return (state & MODIFIED_DURING_EDITING) != 0;
}
public void setSelectAllOnStartEdit(boolean selectAll) {
this.selectAllOnStartEdit = selectAll;
}
public void setEditable(boolean editable) {
if (editable) {
text.setEditable(true);
setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
} else {
text.setEditable(false);
text.setBackground(null);
}
}
public void setText(String text) {
this.text.setText(text);
addToUndoStack(text);
}
private void addToUndoStack(String text) {
if (isTextValid() != null)
return;
String newText = text.trim();
if (undoStack.size() == 0)
undoStack.add(0, newText);
else if (!undoStack.get(0).equals(newText))
undoStack.add(0, newText);
}
public void setTextWithoutNotify(String text) {
this.text.removeModifyListener(listener);
setText(text);
this.text.addModifyListener(listener);
}
public Text getWidget() {
return text;
}
public synchronized void addModifyListener(TextModifyListener listener) {
if (modifyListeners == null) {
modifyListeners = new ListenerList(ListenerList.IDENTITY);
}
modifyListeners.add(listener);
}
public synchronized void removeModifyListener(TextModifyListener listener) {
if (modifyListeners == null)
return;
modifyListeners.remove(listener);
}
public void setInputValidator(IInputValidator validator) {
if (validator != this.validator) {
this.validator = validator;
}
}
private String isTextValid() {
if (validator != null) {
String result = validator.isValid(getWidget().getText());
for(Consumer listener : validationListeners) listener.accept(result);
return result;
}
return null;
}
public void setColorProvider(ITrackedColorProvider provider) {
Assert.isNotNull(provider);
this.colorProvider = provider;
}
private void setBackground(Color background) {
if(text.isDisposed()) return;
if (!text.getEditable()) {
// Do not alter background when the widget is not editable.
return;
}
text.setBackground(background);
}
public boolean isDisposed() {
return text.isDisposed();
}
public Display getDisplay() {
return display;
}
public void addValidationListener(Consumer listener) {
validationListeners.add(listener);
}
public void removeValidationListener(Consumer listener) {
validationListeners.remove(listener);
}
public String getText() {
return text.getText();
}
public int getCaretPosition() {
return text.getCaretPosition();
}
public void undo() {
if (undoStack.size() > 0) {
String lastEdit = undoStack.remove(0);
if (lastEdit.equals(text.getText().trim())) {
if (undoStack.size() == 0)
return;
lastEdit = undoStack.remove(0);
}
String currText = text.getText();
textBeforeEdit = currText;
text.setText(lastEdit);
moveCursorToEnd();
redoStack.add(0, currText);
}
}
public void redo() {
if (redoStack.size() > 0) {
String text = (String) redoStack.remove(0);
moveCursorToEnd();
String currText = this.text.getText();
addToUndoStack(currText);
textBeforeEdit = currText;
this.text.setText(text);
moveCursorToEnd();
}
}
private void moveCursorToEnd() {
text.setSelection(text.getText().length());
}
}