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