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