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