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