]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.utils.ui/src/org/simantics/utils/ui/widgets/TrackedText.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.utils.ui / src / org / simantics / utils / ui / widgets / TrackedText.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.utils.ui.widgets;\r
13 \r
14 import org.eclipse.core.runtime.Assert;\r
15 import org.eclipse.core.runtime.ListenerList;\r
16 import org.eclipse.jface.dialogs.IInputValidator;\r
17 import org.eclipse.jface.resource.ColorDescriptor;\r
18 import org.eclipse.jface.resource.JFaceResources;\r
19 import org.eclipse.jface.resource.LocalResourceManager;\r
20 import org.eclipse.jface.resource.ResourceManager;\r
21 import org.eclipse.swt.SWT;\r
22 import org.eclipse.swt.events.DisposeEvent;\r
23 import org.eclipse.swt.events.DisposeListener;\r
24 import org.eclipse.swt.events.FocusEvent;\r
25 import org.eclipse.swt.events.FocusListener;\r
26 import org.eclipse.swt.events.KeyEvent;\r
27 import org.eclipse.swt.events.KeyListener;\r
28 import org.eclipse.swt.events.ModifyEvent;\r
29 import org.eclipse.swt.events.ModifyListener;\r
30 import org.eclipse.swt.events.MouseEvent;\r
31 import org.eclipse.swt.events.MouseListener;\r
32 import org.eclipse.swt.events.MouseTrackListener;\r
33 import org.eclipse.swt.graphics.Color;\r
34 import org.eclipse.swt.graphics.RGB;\r
35 import org.eclipse.swt.widgets.Composite;\r
36 import org.eclipse.swt.widgets.Text;\r
37 \r
38 /**\r
39  * This is a TrackedTest SWT Text-widget 'decorator'.\r
40  * \r
41  * The widget has 2 main states: editing and inactive.\r
42  * \r
43  * It implements the necessary listeners to achieve the text widget behaviour\r
44  * needed by Simantics. User notification about modifications is provided via\r
45  * {@link TrackedModifyListener}.\r
46  * \r
47  * Examples:\r
48  * \r
49  * <pre>\r
50  * // #1: create new Text internally, use TrackedModifylistener\r
51  * TrackedText trackedText = new TrackedText(parentComposite, style);\r
52  * trackedText.addModifyListener(new TrackedModifyListener() {\r
53  *     public void modifyText(TrackedModifyEvent e) {\r
54  *         // text was modified, do something.\r
55  *     }\r
56  * });\r
57  * \r
58  * // #2: create new Text internally, define the colors for text states.\r
59  * TrackedText trackedText = new TrackedText(text, &lt;instance of ITrackedColorProvider&gt;);\r
60  * </pre>\r
61  * \r
62  * @author Tuukka Lehtonen\r
63  */\r
64 public class TrackedText {\r
65         \r
66         public static final String              TRACKED_TEXT_KEY = "TrackedTextKey";\r
67         \r
68     private static final int      EDITING                 = 1 << 0;\r
69     private static final int      MODIFIED_DURING_EDITING = 1 << 1;\r
70 \r
71     /**\r
72      * Used to tell whether or not a mouseDown has occurred after a focusGained\r
73      * event to be able to select the whole text field when it is pressed for\r
74      * the first time while the widget holds focus.\r
75      */\r
76     private static final int      MOUSE_DOWN_FIRST_TIME   = 1 << 2;\r
77     private static final int      MOUSE_INSIDE_CONTROL    = 1 << 3;\r
78 \r
79     private int                   state;\r
80 \r
81     private int                   caretPositionBeforeEdit;\r
82 \r
83     private String                textBeforeEdit;\r
84 \r
85     private final Text                  text;\r
86 \r
87     private CompositeListener     listener;\r
88 \r
89     private ListenerList          modifyListeners;\r
90 \r
91     private IInputValidator       validator;\r
92 \r
93     private ITrackedColorProvider colorProvider;\r
94 \r
95     private ResourceManager       resourceManager;\r
96 \r
97     private class DefaultColorProvider implements ITrackedColorProvider {\r
98         private final ColorDescriptor highlightColor = ColorDescriptor.createFrom(new RGB(254, 255, 197));\r
99         private final ColorDescriptor inactiveColor = ColorDescriptor.createFrom(new RGB(245, 246, 190));\r
100         private final ColorDescriptor invalidInputColor = ColorDescriptor.createFrom(new RGB(255, 128, 128));\r
101 \r
102         @Override\r
103         public Color getEditingBackground() {\r
104             return null;\r
105         }\r
106 \r
107         @Override\r
108         public Color getHoverBackground() {\r
109             return resourceManager.createColor(highlightColor);\r
110         }\r
111 \r
112         @Override\r
113         public Color getInactiveBackground() {\r
114             return resourceManager.createColor(inactiveColor);\r
115         }\r
116 \r
117         @Override\r
118         public Color getInvalidBackground() {\r
119             return resourceManager.createColor(invalidInputColor);\r
120         }\r
121     };\r
122 \r
123     /**\r
124      * A composite of many UI listeners for creating the functionality of this\r
125      * class.\r
126      */\r
127     private class CompositeListener\r
128     implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,\r
129     MouseListener, FocusListener\r
130     {\r
131         // Keyboard/editing events come in the following order:\r
132         //   1. keyPressed\r
133         //   2. verifyText\r
134         //   3. modifyText\r
135         //   4. keyReleased\r
136 \r
137         public void modifyText(ModifyEvent e) {\r
138             //System.out.println("modifyText: " + e);\r
139             setModified(true);\r
140 \r
141             String valid = isTextValid();\r
142             if (valid != null) {\r
143                 setBackground(colorProvider.getInvalidBackground());\r
144             } else {\r
145                 if (isEditing())\r
146                     setBackground(colorProvider.getEditingBackground());\r
147                 else\r
148                     setBackground(colorProvider.getInactiveBackground());\r
149             }\r
150         }\r
151 \r
152         public void widgetDisposed(DisposeEvent e) {\r
153             getWidget().removeModifyListener(this);\r
154         }\r
155 \r
156         private boolean isMultiLine() {\r
157             return (text.getStyle() & SWT.MULTI) != 0;\r
158         }\r
159 \r
160         private boolean hasMultiLineCommitModifier(KeyEvent e) {\r
161             return (e.stateMask & SWT.CTRL) != 0;\r
162         }\r
163 \r
164         public void keyPressed(KeyEvent e) {\r
165             //System.out.println("keyPressed: " + e);\r
166             if (!isEditing()) {\r
167                 // ESC, ENTER & keypad ENTER must not start editing\r
168                 if (e.keyCode == SWT.ESC)\r
169                     return;\r
170 \r
171                 if (!isMultiLine()) {\r
172                     if (e.keyCode == SWT.F2 || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
173                         startEdit(true);\r
174                     } else if (e.character != '\0') {\r
175                         startEdit(false);\r
176                     }\r
177                 } else {\r
178                     // In multi-line mode, TAB must not start editing!\r
179                     if (e.keyCode == SWT.F2) {\r
180                         startEdit(true);\r
181                     } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
182                         if (hasMultiLineCommitModifier(e)) {\r
183                             e.doit = false;\r
184                         } else {\r
185                             startEdit(false);\r
186                         }\r
187                     } else if (e.keyCode == SWT.TAB) {\r
188                         text.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT);\r
189                         e.doit = false;\r
190                     } else if (e.character != '\0') {\r
191                         startEdit(false);\r
192                     }\r
193                 }\r
194             } else {\r
195                 // ESC reverts any changes made during this edit\r
196                 if (e.keyCode == SWT.ESC) {\r
197                     revertEdit();\r
198                 }\r
199                 if (!isMultiLine()) {\r
200                     if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
201                         applyEdit();\r
202                     }\r
203                 } else {\r
204                     if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
205                         if (hasMultiLineCommitModifier(e)) {\r
206                             applyEdit();\r
207                             e.doit = false;\r
208                         }\r
209                     }\r
210                 }\r
211             }\r
212         }\r
213 \r
214         public void keyReleased(KeyEvent e) {\r
215             //System.out.println("keyReleased: " + e);\r
216         }\r
217 \r
218         public void mouseEnter(MouseEvent e) {\r
219             //System.out.println("mouseEnter");\r
220             if (!isEditing()) {\r
221                 setBackground(colorProvider.getHoverBackground());\r
222             }\r
223             setMouseInsideControl(true);\r
224         }\r
225 \r
226         public void mouseExit(MouseEvent e) {\r
227             //System.out.println("mouseExit");\r
228             if (!isEditing()) {\r
229                 setBackground(colorProvider.getInactiveBackground());\r
230             }\r
231             setMouseInsideControl(false);\r
232         }\r
233 \r
234         public void mouseHover(MouseEvent e) {\r
235             //System.out.println("mouseHover");\r
236             setMouseInsideControl(true);\r
237         }\r
238 \r
239         public void mouseDoubleClick(MouseEvent e) {\r
240             //System.out.println("mouseDoubleClick: " + e);\r
241             if (e.button == 1) {\r
242                 getWidget().selectAll();\r
243             }\r
244         }\r
245 \r
246         public void mouseDown(MouseEvent e) {\r
247             //System.out.println("mouseDown: " + e);\r
248             if (!isEditing()) {\r
249                 // In reality we should never get here, since focusGained\r
250                 // always comes before mouseDown, but let's keep this\r
251                 // fallback just to be safe.\r
252                 if (e.button == 1) {\r
253                     startEdit(true);\r
254                 }\r
255             } else {\r
256                 if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {\r
257                     getWidget().selectAll();\r
258                     state &= ~MOUSE_DOWN_FIRST_TIME;\r
259                 }\r
260             }\r
261         }\r
262 \r
263         public void mouseUp(MouseEvent e) {\r
264         }\r
265 \r
266         public void focusGained(FocusEvent e) {\r
267             //System.out.println("focusGained");\r
268             if (!isEditing()) {\r
269                 if (!isMultiLine()) {\r
270                     // Always start edit on single line texts when focus is gained\r
271                     startEdit(true);\r
272                 }\r
273             }\r
274         }\r
275 \r
276         public void focusLost(FocusEvent e) {\r
277             //System.out.println("focusLost");\r
278             if (isEditing()) {\r
279                 applyEdit();\r
280             }\r
281         }\r
282     }\r
283 \r
284     public TrackedText(Text text) {\r
285         Assert.isNotNull(text);\r
286         this.state = 0;\r
287         this.text = text;\r
288         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);\r
289         this.colorProvider = new DefaultColorProvider();\r
290 \r
291         initialize();\r
292     }\r
293 \r
294     public TrackedText(Composite parent, int style) {\r
295         this.state = 0;\r
296         this.text = new Text(parent, style);\r
297         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);\r
298         this.colorProvider = new DefaultColorProvider();\r
299 \r
300         initialize();\r
301     }\r
302 \r
303     public TrackedText(Text text, ITrackedColorProvider colorProvider) {\r
304         Assert.isNotNull(text, "text must not be null");\r
305         Assert.isNotNull(colorProvider, "colorProvider must not be null");\r
306         this.state = 0;\r
307         this.text = text;\r
308         this.colorProvider = colorProvider;\r
309 \r
310         initialize();\r
311     }\r
312 \r
313     public TrackedText(Composite parent, int style, ITrackedColorProvider colorProvider) {\r
314         Assert.isNotNull(colorProvider, "colorProvider must not be null");\r
315         this.state = 0;\r
316         this.text = new Text(parent, style);\r
317         this.colorProvider = colorProvider;\r
318 \r
319         initialize();\r
320     }\r
321 \r
322     /**\r
323      * Common initialization. Assumes that text is already created.\r
324      */\r
325     private void initialize() {\r
326         Assert.isNotNull(text);\r
327 \r
328         text.setBackground(colorProvider.getInactiveBackground());\r
329         text.setDoubleClickEnabled(false);\r
330         text.setData(TRACKED_TEXT_KEY, this);\r
331 \r
332         listener = new CompositeListener();\r
333 \r
334         text.addModifyListener(listener);\r
335         text.addDisposeListener(listener);\r
336         text.addKeyListener(listener);\r
337         text.addMouseTrackListener(listener);\r
338         text.addMouseListener(listener);\r
339         text.addFocusListener(listener);\r
340     }\r
341 \r
342     private void startEdit(boolean selectAll) {\r
343         if (isEditing()) {\r
344             // Print some debug incase we end are in an invalid state\r
345             System.out.println("TrackedText: BUG: startEdit called when in editing state");\r
346         }\r
347         //System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);\r
348 \r
349         // Backup text-field data for reverting purposes\r
350         caretPositionBeforeEdit = text.getCaretPosition();\r
351         textBeforeEdit = text.getText();\r
352 \r
353         // Signal editing state\r
354         setBackground(colorProvider.getEditingBackground());\r
355 \r
356         if (selectAll) {\r
357             text.selectAll();\r
358         }\r
359         state |= EDITING | MOUSE_DOWN_FIRST_TIME;\r
360     }\r
361 \r
362     private void applyEdit() {\r
363         try {\r
364             if (isTextValid() != null) {\r
365                 text.setText(textBeforeEdit);\r
366             } else if (isModified() && !text.getText().equals(textBeforeEdit)) {\r
367                 //System.out.println("apply");\r
368                 if (modifyListeners != null) {\r
369                     TrackedModifyEvent event = new TrackedModifyEvent(text, text.getText());\r
370                     for (Object o : modifyListeners.getListeners()) {\r
371                         ((TrackedModifyListener) o).modifyText(event);\r
372                     }\r
373                 }\r
374             }\r
375         } finally {\r
376             endEdit();\r
377         }\r
378     }\r
379 \r
380     private void endEdit() {\r
381         if (!isEditing()) {\r
382             // Print some debug incase we end are in an invalid state\r
383             //ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state"));\r
384         }\r
385         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
386         //System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection());\r
387         // Always move the caret to the end of the string\r
388         text.setSelection(text.getCharCount());\r
389         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
390         setModified(false);\r
391     }\r
392 \r
393     private void revertEdit() {\r
394         if (!isEditing()) {\r
395             // Print some debug incase we end are in an invalid state\r
396             //ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state"));\r
397             System.out.println("BUG: revertEdit called when not in editing state");\r
398         }\r
399         text.setText(textBeforeEdit);\r
400         text.setSelection(caretPositionBeforeEdit);\r
401         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
402         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
403         setModified(false);\r
404     }\r
405 \r
406     public boolean isEditing() {\r
407         return (state & EDITING) != 0;\r
408     }\r
409 \r
410     private void setModified(boolean modified) {\r
411         if (modified) {\r
412             state |= MODIFIED_DURING_EDITING;\r
413         } else {\r
414             state &= ~MODIFIED_DURING_EDITING;\r
415         }\r
416     }\r
417 \r
418     private boolean isMouseInsideControl() {\r
419         return (state & MOUSE_INSIDE_CONTROL) != 0;\r
420     }\r
421 \r
422     private void setMouseInsideControl(boolean inside) {\r
423         if (inside)\r
424             state |= MOUSE_INSIDE_CONTROL;\r
425         else\r
426             state &= ~MOUSE_INSIDE_CONTROL;\r
427     }\r
428 \r
429     public boolean isModified() {\r
430         return (state & MODIFIED_DURING_EDITING) != 0;\r
431     }\r
432 \r
433     public void setEditable(boolean editable) {\r
434         if (editable) {\r
435             text.setEditable(true);\r
436             setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
437         } else {\r
438             text.setEditable(false);\r
439             text.setBackground(null);\r
440         }\r
441     }\r
442 \r
443     public void setText(String text) {\r
444         this.text.setText(text);\r
445     }\r
446 \r
447     public void setTextWithoutNotify(String text) {\r
448         this.text.removeModifyListener(listener);\r
449         setText(text);\r
450         this.text.addModifyListener(listener);\r
451     }\r
452 \r
453     public Text getWidget() {\r
454         return text;\r
455     }\r
456 \r
457     public synchronized void addModifyListener(TrackedModifyListener listener) {\r
458         if (modifyListeners == null) {\r
459             modifyListeners = new ListenerList(ListenerList.IDENTITY);\r
460         }\r
461         modifyListeners.add(listener);\r
462     }\r
463 \r
464     public synchronized void removeModifyListener(TrackedModifyListener listener) {\r
465         if (modifyListeners == null)\r
466             return;\r
467         modifyListeners.remove(listener);\r
468     }\r
469 \r
470     public void setInputValidator(IInputValidator validator) {\r
471         if (validator != this.validator) {\r
472             this.validator = validator;\r
473         }\r
474     }\r
475 \r
476     private String isTextValid() {\r
477         if (validator != null) {\r
478             return validator.isValid(getWidget().getText());\r
479         }\r
480         return null;\r
481     }\r
482 \r
483     public void setColorProvider(ITrackedColorProvider provider) {\r
484         Assert.isNotNull(provider);\r
485         this.colorProvider = provider;\r
486     }\r
487 \r
488     public void updateColor() {\r
489         if (isEditing()) {\r
490             // Editing logic should take care of setting the appropriate\r
491             // background color.\r
492         } else if (!text.getEditable()) {\r
493             text.setBackground(null);\r
494         } else {\r
495             setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
496         }\r
497     }\r
498 \r
499     private void setBackground(Color background) {\r
500         if (!text.getEditable() || (background != null && background.isDisposed())) {\r
501             // Do not alter background when the widget is not editable.\r
502             return;\r
503         }\r
504         text.setBackground(background);\r
505     }\r
506 \r
507 }\r