-/*******************************************************************************\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, <instance of ITrackedColorProvider>);\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, <instance of ITrackedColorProvider>);
+ * </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());
+ }
+}