--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 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.utils.ui.widgets;\r
+\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.ColorDescriptor;\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.RGB;\r
+import org.eclipse.swt.widgets.Composite;\r
+import org.eclipse.swt.widgets.Text;\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 TrackedModifyListener}.\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 {\r
+ \r
+ public static final String TRACKED_TEXT_KEY = "TrackedTextKey";\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 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 ResourceManager resourceManager;\r
+\r
+ private class DefaultColorProvider implements ITrackedColorProvider {\r
+ private final ColorDescriptor highlightColor = ColorDescriptor.createFrom(new RGB(254, 255, 197));\r
+ private final ColorDescriptor inactiveColor = ColorDescriptor.createFrom(new RGB(245, 246, 190));\r
+ private final ColorDescriptor invalidInputColor = ColorDescriptor.createFrom(new RGB(255, 128, 128));\r
+\r
+ @Override\r
+ public Color getEditingBackground() {\r
+ return null;\r
+ }\r
+\r
+ @Override\r
+ public Color getHoverBackground() {\r
+ return resourceManager.createColor(highlightColor);\r
+ }\r
+\r
+ @Override\r
+ public Color getInactiveBackground() {\r
+ return resourceManager.createColor(inactiveColor);\r
+ }\r
+\r
+ @Override\r
+ public Color getInvalidBackground() {\r
+ return resourceManager.createColor(invalidInputColor);\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
+ 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
+ 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
+ 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(true);\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(true);\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
+ public void keyReleased(KeyEvent e) {\r
+ //System.out.println("keyReleased: " + e);\r
+ }\r
+\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
+ public void mouseExit(MouseEvent e) {\r
+ //System.out.println("mouseExit");\r
+ if (!isEditing()) {\r
+ setBackground(colorProvider.getInactiveBackground());\r
+ }\r
+ setMouseInsideControl(false);\r
+ }\r
+\r
+ public void mouseHover(MouseEvent e) {\r
+ //System.out.println("mouseHover");\r
+ setMouseInsideControl(true);\r
+ }\r
+\r
+ public void mouseDoubleClick(MouseEvent e) {\r
+ //System.out.println("mouseDoubleClick: " + e);\r
+ if (e.button == 1) {\r
+ getWidget().selectAll();\r
+ }\r
+ }\r
+\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(true);\r
+ }\r
+ } else {\r
+ if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {\r
+ getWidget().selectAll();\r
+ state &= ~MOUSE_DOWN_FIRST_TIME;\r
+ }\r
+ }\r
+ }\r
+\r
+ public void mouseUp(MouseEvent e) {\r
+ }\r
+\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(true);\r
+ }\r
+ }\r
+ }\r
+\r
+ public void focusLost(FocusEvent e) {\r
+ //System.out.println("focusLost");\r
+ if (isEditing()) {\r
+ applyEdit();\r
+ }\r
+ }\r
+ }\r
+\r
+ public TrackedText(Text text) {\r
+ Assert.isNotNull(text);\r
+ this.state = 0;\r
+ this.text = text;\r
+ this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);\r
+ this.colorProvider = new DefaultColorProvider();\r
+\r
+ initialize();\r
+ }\r
+\r
+ public TrackedText(Composite parent, int style) {\r
+ this.state = 0;\r
+ this.text = new Text(parent, style);\r
+ this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);\r
+ this.colorProvider = new DefaultColorProvider();\r
+\r
+ initialize();\r
+ }\r
+\r
+ public TrackedText(Text text, ITrackedColorProvider colorProvider) {\r
+ Assert.isNotNull(text, "text must not be null");\r
+ Assert.isNotNull(colorProvider, "colorProvider must not be null");\r
+ this.state = 0;\r
+ this.text = text;\r
+ this.colorProvider = colorProvider;\r
+\r
+ initialize();\r
+ }\r
+\r
+ public TrackedText(Composite parent, int style, ITrackedColorProvider colorProvider) {\r
+ Assert.isNotNull(colorProvider, "colorProvider must not be null");\r
+ this.state = 0;\r
+ this.text = new Text(parent, style);\r
+ this.colorProvider = colorProvider;\r
+\r
+ initialize();\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
+ text.setData(TRACKED_TEXT_KEY, this);\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
+ private 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
+ ((TrackedModifyListener) o).modifyText(event);\r
+ }\r
+ }\r
+ }\r
+ } finally {\r
+ endEdit();\r
+ }\r
+ }\r
+\r
+ private void endEdit() {\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
+ }\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
+ 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
+ public 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
+ public boolean isModified() {\r
+ return (state & MODIFIED_DURING_EDITING) != 0;\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
+ }\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(TrackedModifyListener listener) {\r
+ if (modifyListeners == null) {\r
+ modifyListeners = new ListenerList(ListenerList.IDENTITY);\r
+ }\r
+ modifyListeners.add(listener);\r
+ }\r
+\r
+ public synchronized void removeModifyListener(TrackedModifyListener 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
+ return validator.isValid(getWidget().getText());\r
+ }\r
+ return null;\r
+ }\r
+\r
+ public void setColorProvider(ITrackedColorProvider provider) {\r
+ Assert.isNotNull(provider);\r
+ this.colorProvider = provider;\r
+ }\r
+\r
+ public void updateColor() {\r
+ if (isEditing()) {\r
+ // Editing logic should take care of setting the appropriate\r
+ // background color.\r
+ } else if (!text.getEditable()) {\r
+ text.setBackground(null);\r
+ } else {\r
+ setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
+ }\r
+ }\r
+\r
+ private void setBackground(Color background) {\r
+ if (!text.getEditable() || (background != null && background.isDisposed())) {\r
+ // Do not alter background when the widget is not editable.\r
+ return;\r
+ }\r
+ text.setBackground(background);\r
+ }\r
+\r
+}\r