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