/******************************************************************************* * 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 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 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()); } }