]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/widgets/TrackedText.java
7ab5a50016afdc52ba21346aa288a0ee0e3f8188
[simantics/platform.git] / bundles / org.simantics.browsing.ui.swt / src / org / simantics / browsing / ui / swt / widgets / TrackedText.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2012 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.browsing.ui.swt.widgets;\r
13 \r
14 import java.util.LinkedList;\r
15 import java.util.List;\r
16 import java.util.concurrent.CopyOnWriteArrayList;\r
17 import java.util.function.Consumer;\r
18 \r
19 import org.eclipse.core.commands.Command;\r
20 import org.eclipse.core.commands.State;\r
21 import org.eclipse.core.runtime.Assert;\r
22 import org.eclipse.core.runtime.ListenerList;\r
23 import org.eclipse.jface.dialogs.IInputValidator;\r
24 import org.eclipse.jface.resource.JFaceResources;\r
25 import org.eclipse.jface.resource.LocalResourceManager;\r
26 import org.eclipse.jface.resource.ResourceManager;\r
27 import org.eclipse.swt.SWT;\r
28 import org.eclipse.swt.events.DisposeEvent;\r
29 import org.eclipse.swt.events.DisposeListener;\r
30 import org.eclipse.swt.events.FocusEvent;\r
31 import org.eclipse.swt.events.FocusListener;\r
32 import org.eclipse.swt.events.KeyEvent;\r
33 import org.eclipse.swt.events.KeyListener;\r
34 import org.eclipse.swt.events.ModifyEvent;\r
35 import org.eclipse.swt.events.ModifyListener;\r
36 import org.eclipse.swt.events.MouseEvent;\r
37 import org.eclipse.swt.events.MouseListener;\r
38 import org.eclipse.swt.events.MouseTrackListener;\r
39 import org.eclipse.swt.graphics.Color;\r
40 import org.eclipse.swt.graphics.Font;\r
41 import org.eclipse.swt.widgets.Composite;\r
42 import org.eclipse.swt.widgets.Display;\r
43 import org.eclipse.swt.widgets.Text;\r
44 import org.eclipse.ui.PlatformUI;\r
45 import org.eclipse.ui.commands.ICommandService;\r
46 import org.simantics.browsing.ui.swt.widgets.impl.ITrackedColorProvider;\r
47 import org.simantics.browsing.ui.swt.widgets.impl.ReadFactory;\r
48 import org.simantics.browsing.ui.swt.widgets.impl.TextModifyListener;\r
49 import org.simantics.browsing.ui.swt.widgets.impl.TrackedModifyEvent;\r
50 import org.simantics.browsing.ui.swt.widgets.impl.Widget;\r
51 import org.simantics.browsing.ui.swt.widgets.impl.WidgetSupport;\r
52 import org.simantics.db.management.ISessionContext;\r
53 import org.simantics.db.procedure.Listener;\r
54 import org.simantics.ui.states.TrackedTextState;\r
55 \r
56 /**\r
57  * This is a TrackedTest SWT Text-widget 'decorator'.\r
58  * \r
59  * The widget has 2 main states: editing and inactive.\r
60  * \r
61  * It implements the necessary listeners to achieve the text widget behaviour\r
62  * needed by Simantics. User notification about modifications is provided via\r
63  * {@link TextModifyListener}.\r
64  * \r
65  * Examples:\r
66  * \r
67  * <pre>\r
68  * // #1: create new Text internally, use TrackedModifylistener\r
69  * TrackedText trackedText = new TrackedText(parentComposite, style);\r
70  * trackedText.addModifyListener(new TrackedModifyListener() {\r
71  *     public void modifyText(TrackedModifyEvent e) {\r
72  *         // text was modified, do something.\r
73  *     }\r
74  * });\r
75  * \r
76  * // #2: create new Text internally, define the colors for text states.\r
77  * TrackedText trackedText = new TrackedText(text, &lt;instance of ITrackedColorProvider&gt;);\r
78  * </pre>\r
79  * \r
80  * @author Tuukka Lehtonen\r
81  */\r
82 public class TrackedText implements Widget {\r
83     \r
84     public static final String    ID = "TRACKED_TEXT";\r
85     \r
86     private static final int      EDITING                 = 1 << 0;\r
87     private static final int      MODIFIED_DURING_EDITING = 1 << 1;\r
88 \r
89     /**\r
90      * Used to tell whether or not a mouseDown has occurred after a focusGained\r
91      * event to be able to select the whole text field when it is pressed for\r
92      * the first time while the widget holds focus.\r
93      */\r
94     private static final int      MOUSE_DOWN_FIRST_TIME   = 1 << 2;\r
95     private static final int      MOUSE_INSIDE_CONTROL    = 1 << 3;\r
96 \r
97     private int                   state;\r
98 \r
99     private int                   caretPositionBeforeEdit;\r
100 \r
101     private String                textBeforeEdit;\r
102 \r
103     private final Display         display;\r
104 \r
105     private final Text            text;\r
106 \r
107     private CompositeListener     listener;\r
108 \r
109     private ListenerList          modifyListeners;\r
110 \r
111     private IInputValidator       validator;\r
112 \r
113     private ITrackedColorProvider colorProvider;\r
114 \r
115     private final ResourceManager resourceManager;\r
116 \r
117         private ReadFactory<?, String> textFactory;\r
118         \r
119         private boolean moveCaretAfterEdit = true;\r
120         \r
121         private boolean selectAllOnStartEdit = true;\r
122         \r
123         private final CopyOnWriteArrayList<Consumer<String>> validationListeners = new CopyOnWriteArrayList<>(); \r
124         \r
125         \r
126         // UNDO REDO HANDLER\r
127         \r
128         private static final int MAX_STACK_SIZE = 25;\r
129 \r
130         private List<String> undoStack = new LinkedList<String>();\r
131         private List<String> redoStack = new LinkedList<String>();\r
132         \r
133         public void setTextFactory(ReadFactory<?, String> textFactory) {\r
134                 this.textFactory = textFactory;\r
135         }\r
136     \r
137         public void setFont(Font font) {\r
138                 text.setFont(font);\r
139         }\r
140         \r
141         public void setMoveCaretAfterEdit(boolean value) {\r
142                 this.moveCaretAfterEdit = value;\r
143         }\r
144         \r
145         @Override\r
146         public void setInput(ISessionContext context, Object input) {\r
147 \r
148         if (modifyListeners != null) {\r
149             for (Object o : modifyListeners.getListeners()) {\r
150                 if(o instanceof Widget) {\r
151                     ((Widget) o).setInput(context, input);\r
152                 }\r
153             }\r
154         }\r
155                 \r
156                 if(textFactory != null) {\r
157                         textFactory.listen(context, input, new Listener<String>() {\r
158 \r
159                                 @Override\r
160                 public void exception(final Throwable t) {\r
161                                         display.asyncExec(new Runnable() {\r
162 \r
163                                                 @Override\r
164                                                 public void run() {\r
165                                                         if(isDisposed()) return;\r
166 //                                                      System.out.println("Button received new text: " + text);\r
167                                                         text.setText(t.toString());\r
168                                                 }\r
169 \r
170                                         });\r
171                                 }\r
172 \r
173                                 @Override\r
174                                 public void execute(final String string) {\r
175                                         \r
176                                         if(text.isDisposed()) return;\r
177                                         \r
178                                         display.asyncExec(new Runnable() {\r
179 \r
180                                                 @Override\r
181                                                 public void run() {\r
182                                                         if(isDisposed()) return;\r
183                                                         text.setText(string == null ? "" : string);\r
184 //                                                      text.getParent().layout();\r
185 //                                                      text.getParent().getParent().layout();\r
186                                                 }\r
187 \r
188                                         });\r
189                                 }\r
190 \r
191                                 @Override\r
192                                 public boolean isDisposed() {\r
193                                         return text.isDisposed();\r
194                                 }\r
195 \r
196                         });\r
197                 }\r
198                 \r
199         }\r
200         \r
201     /**\r
202      * A composite of many UI listeners for creating the functionality of this\r
203      * class.\r
204      */\r
205     private class CompositeListener\r
206     implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,\r
207     MouseListener, FocusListener\r
208     {\r
209         // Keyboard/editing events come in the following order:\r
210         //   1. keyPressed\r
211         //   2. verifyText\r
212         //   3. modifyText\r
213         //   4. keyReleased\r
214 \r
215         @Override\r
216         public void modifyText(ModifyEvent e) {\r
217             //System.out.println("modifyText: " + e);\r
218             setModified(true);\r
219 \r
220             String valid = isTextValid();\r
221             if (valid != null) {\r
222                 setBackground(colorProvider.getInvalidBackground());\r
223             } else {\r
224                 if (isEditing())\r
225                     setBackground(colorProvider.getEditingBackground());\r
226                 else\r
227                     setBackground(colorProvider.getInactiveBackground());\r
228             }\r
229         }\r
230 \r
231         @Override\r
232         public void widgetDisposed(DisposeEvent e) {\r
233             getWidget().removeModifyListener(this);\r
234         }\r
235 \r
236         private boolean isMultiLine() {\r
237             return (text.getStyle() & SWT.MULTI) != 0;\r
238         }\r
239 \r
240         private boolean hasMultiLineCommitModifier(KeyEvent e) {\r
241             return (e.stateMask & SWT.CTRL) != 0;\r
242         }\r
243 \r
244         @Override\r
245         public void keyPressed(KeyEvent e) {\r
246             //System.out.println("keyPressed: " + e);\r
247             if (!isEditing()) {\r
248                 // ESC, ENTER & keypad ENTER must not start editing\r
249                 if (e.keyCode == SWT.ESC)\r
250                     return;\r
251 \r
252                 if (!isMultiLine()) {\r
253                     if (e.keyCode == SWT.F2 || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
254                         startEdit(selectAllOnStartEdit);\r
255                     } else if (e.character != '\0') {\r
256                         startEdit(false);\r
257                     }\r
258                 } else {\r
259                     // In multi-line mode, TAB must not start editing!\r
260                     if (e.keyCode == SWT.F2) {\r
261                         startEdit(selectAllOnStartEdit);\r
262                     } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
263                         if (hasMultiLineCommitModifier(e)) {\r
264                             e.doit = false;\r
265                         } else {\r
266                             startEdit(false);\r
267                         }\r
268                     } else if (e.keyCode == SWT.TAB) {\r
269                         text.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT);\r
270                         e.doit = false;\r
271                     } else if (e.character != '\0') {\r
272                         startEdit(false);\r
273                     }\r
274                 }\r
275             } else {\r
276                 // ESC reverts any changes made during this edit\r
277                 if (e.keyCode == SWT.ESC) {\r
278                     revertEdit();\r
279                 }\r
280                 if (!isMultiLine()) {\r
281                     if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
282                         applyEdit();\r
283                     }\r
284                 } else {\r
285                     if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {\r
286                         if (hasMultiLineCommitModifier(e)) {\r
287                             applyEdit();\r
288                             e.doit = false;\r
289                         }\r
290                     }\r
291                 }\r
292             }\r
293         }\r
294 \r
295         @Override\r
296         public void keyReleased(KeyEvent e) {\r
297             //System.out.println("keyReleased: " + e);\r
298         }\r
299 \r
300         @Override\r
301         public void mouseEnter(MouseEvent e) {\r
302             //System.out.println("mouseEnter");\r
303             if (!isEditing()) {\r
304                 setBackground(colorProvider.getHoverBackground());\r
305             }\r
306             setMouseInsideControl(true);\r
307         }\r
308 \r
309         @Override\r
310         public void mouseExit(MouseEvent e) {\r
311             //System.out.println("mouseExit");\r
312             if (!isEditing()) {\r
313                 setBackground(colorProvider.getInactiveBackground());\r
314             }\r
315             setMouseInsideControl(false);\r
316         }\r
317 \r
318         @Override\r
319         public void mouseHover(MouseEvent e) {\r
320             //System.out.println("mouseHover");\r
321             setMouseInsideControl(true);\r
322         }\r
323 \r
324         @Override\r
325         public void mouseDoubleClick(MouseEvent e) {\r
326             //System.out.println("mouseDoubleClick: " + e);\r
327             if (e.button == 1) {\r
328                 getWidget().selectAll();\r
329             }\r
330         }\r
331 \r
332         @Override\r
333         public void mouseDown(MouseEvent e) {\r
334             //System.out.println("mouseDown: " + e);\r
335             if (!isEditing()) {\r
336                 // In reality we should never get here, since focusGained\r
337                 // always comes before mouseDown, but let's keep this\r
338                 // fallback just to be safe.\r
339                 if (e.button == 1) {\r
340                     startEdit(selectAllOnStartEdit);\r
341                 }\r
342             } else {\r
343                 if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {\r
344                     if (!isMultiLine()) {\r
345                         // This is useless for multi-line texts\r
346                         getWidget().selectAll();\r
347                     }\r
348                     state &= ~MOUSE_DOWN_FIRST_TIME;\r
349                 }\r
350             }\r
351         }\r
352 \r
353         @Override\r
354         public void mouseUp(MouseEvent e) {\r
355         }\r
356 \r
357         @Override\r
358         public void focusGained(FocusEvent e) {\r
359             //System.out.println("focusGained");\r
360             if (!isEditing()) {\r
361                 if (!isMultiLine()) {\r
362                     // Always start edit on single line texts when focus is gained\r
363                     startEdit(selectAllOnStartEdit);\r
364                 }\r
365             }\r
366         }\r
367 \r
368         @Override\r
369         public void focusLost(FocusEvent e) {\r
370             //System.out.println("focusLost");\r
371             if (isEditing()) {\r
372                 applyEdit();\r
373             }\r
374         }\r
375     }\r
376 \r
377     public TrackedText(Composite parent, WidgetSupport support, int style) {\r
378         this.state = 0;\r
379         this.text = new Text(parent, style);\r
380         text.setData(ID, this);\r
381         this.display = text.getDisplay();\r
382         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);\r
383         this.colorProvider = new DefaultColorProvider(resourceManager);\r
384         if (support!=null) support.register(this);\r
385         initialize();\r
386         \r
387         createUndoRedoHandler();\r
388     }\r
389     \r
390     private void createUndoRedoHandler() {\r
391         \r
392         text.addModifyListener(new ModifyListener() {\r
393             \r
394             private int eventTimeOut = 1000;\r
395             private long lastEventTimeStamp = 0;\r
396             \r
397             @Override\r
398             public void modifyText(ModifyEvent event) {\r
399                 String newText = text.getText().trim();\r
400                 if (event.time - lastEventTimeStamp > eventTimeOut || newText.endsWith(" ")) {\r
401                     if (newText != null && newText.length() > 0) {\r
402                       if (undoStack.size() == MAX_STACK_SIZE) {\r
403                           undoStack.remove(undoStack.size() - 1);\r
404                       }\r
405                       addToUndoStack(newText);\r
406                     }\r
407                 }\r
408                 lastEventTimeStamp = (event.time & 0xFFFFFFFFL);\r
409               }\r
410         });\r
411 \r
412         text.addFocusListener(new FocusListener() {\r
413             \r
414             @Override\r
415             public void focusLost(FocusEvent e) {\r
416                 ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);\r
417                 Command command = service.getCommand( TrackedTextState.COMMAND_ID );\r
418                 State state = command.getState( TrackedTextState.STATE_ID );\r
419                 state.setValue(true);\r
420             }\r
421             \r
422             @Override\r
423             public void focusGained(FocusEvent e) {\r
424                 addToUndoStack(text.getText());\r
425                 ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);\r
426                 Command command = service.getCommand( TrackedTextState.COMMAND_ID );\r
427                 State state = command.getState( TrackedTextState.STATE_ID );\r
428                 state.setValue(false);\r
429             }\r
430         });\r
431     }\r
432 \r
433     public ResourceManager getResourceManager() {\r
434         return resourceManager;\r
435     }\r
436 \r
437     /**\r
438      * Common initialization. Assumes that text is already created.\r
439      */\r
440     private void initialize() {\r
441         Assert.isNotNull(text);\r
442 \r
443         text.setBackground(colorProvider.getInactiveBackground());\r
444         text.setDoubleClickEnabled(false);\r
445 \r
446         listener = new CompositeListener();\r
447 \r
448         text.addModifyListener(listener);\r
449         text.addDisposeListener(listener);\r
450         text.addKeyListener(listener);\r
451         text.addMouseTrackListener(listener);\r
452         text.addMouseListener(listener);\r
453         text.addFocusListener(listener);\r
454     }\r
455 \r
456     public void startEdit(boolean selectAll) {\r
457         if (isEditing()) {\r
458             // Print some debug incase we end are in an invalid state\r
459             System.out.println("TrackedText: BUG: startEdit called when in editing state");\r
460         }\r
461 //        System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);\r
462 \r
463         // Backup text-field data for reverting purposes\r
464         caretPositionBeforeEdit = text.getCaretPosition();\r
465         textBeforeEdit = text.getText();\r
466 \r
467         // Signal editing state\r
468         setBackground(colorProvider.getEditingBackground());\r
469 \r
470         if (selectAll) {\r
471             text.selectAll();\r
472         }\r
473         state |= EDITING | MOUSE_DOWN_FIRST_TIME;\r
474     }\r
475 \r
476     private void applyEdit() {\r
477         try {\r
478             if (isTextValid() != null) {\r
479                 text.setText(textBeforeEdit);\r
480             } else if (isModified() && !text.getText().equals(textBeforeEdit)) {\r
481                 //System.out.println("apply");\r
482                 if (modifyListeners != null) {\r
483                     TrackedModifyEvent event = new TrackedModifyEvent(text, text.getText());\r
484                     for (Object o : modifyListeners.getListeners()) {\r
485                         ((TextModifyListener) o).modifyText(event);\r
486                     }\r
487                     moveCursorToEnd();\r
488                 }\r
489             }\r
490         } catch (Throwable t) {\r
491             t.printStackTrace();\r
492         } finally {\r
493             endEdit();\r
494         }\r
495     }\r
496 \r
497     private void endEdit() {\r
498         if (text.isDisposed())\r
499             return;\r
500 \r
501         if (!isEditing()) {\r
502             // Print some debug incase we end are in an invalid state\r
503             //ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state"));\r
504             //System.out.println();\r
505         }\r
506         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
507 //        System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection());\r
508         // Always move the caret to the end of the string\r
509         if(moveCaretAfterEdit)\r
510             text.setSelection(text.getCharCount());\r
511         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
512         setModified(false);\r
513     }\r
514 \r
515     private void revertEdit() {\r
516         if (!isEditing()) {\r
517             // Print some debug incase we end are in an invalid state\r
518             //ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state"));\r
519             System.out.println("BUG: revertEdit called when not in editing state");\r
520         }\r
521         text.setText(textBeforeEdit);\r
522         text.setSelection(caretPositionBeforeEdit);\r
523         setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
524         state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);\r
525         setModified(false);\r
526     }\r
527 \r
528     private boolean isEditing() {\r
529         return (state & EDITING) != 0;\r
530     }\r
531 \r
532     private void setModified(boolean modified) {\r
533         if (modified) {\r
534             state |= MODIFIED_DURING_EDITING;\r
535         } else {\r
536             state &= ~MODIFIED_DURING_EDITING;\r
537         }\r
538     }\r
539 \r
540     private boolean isMouseInsideControl() {\r
541         return (state & MOUSE_INSIDE_CONTROL) != 0;\r
542     }\r
543 \r
544     private void setMouseInsideControl(boolean inside) {\r
545         if (inside)\r
546             state |= MOUSE_INSIDE_CONTROL;\r
547         else\r
548             state &= ~MOUSE_INSIDE_CONTROL;\r
549     }\r
550 \r
551     private boolean isModified() {\r
552         return (state & MODIFIED_DURING_EDITING) != 0;\r
553     }\r
554 \r
555     public void setSelectAllOnStartEdit(boolean selectAll) {\r
556         this.selectAllOnStartEdit = selectAll;\r
557     }\r
558     \r
559     public void setEditable(boolean editable) {\r
560         if (editable) {\r
561             text.setEditable(true);\r
562             setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());\r
563         } else {\r
564             text.setEditable(false);\r
565             text.setBackground(null);\r
566         }\r
567     }\r
568 \r
569     public void setText(String text) {\r
570         this.text.setText(text);\r
571         addToUndoStack(text);\r
572     }\r
573 \r
574     private void addToUndoStack(String text) {\r
575         if (isTextValid() != null)\r
576             return;\r
577         String newText = text.trim();\r
578         if (undoStack.size() == 0)\r
579             undoStack.add(0, newText);\r
580         else if (!undoStack.get(0).equals(newText))\r
581             undoStack.add(0, newText);\r
582     }\r
583 \r
584     public void setTextWithoutNotify(String text) {\r
585         this.text.removeModifyListener(listener);\r
586         setText(text);\r
587         this.text.addModifyListener(listener);\r
588     }\r
589 \r
590     public Text getWidget() {\r
591         return text;\r
592     }\r
593 \r
594     public synchronized void addModifyListener(TextModifyListener listener) {\r
595         if (modifyListeners == null) {\r
596             modifyListeners = new ListenerList(ListenerList.IDENTITY);\r
597         }\r
598         modifyListeners.add(listener);\r
599     }\r
600 \r
601     public synchronized void removeModifyListener(TextModifyListener listener) {\r
602         if (modifyListeners == null)\r
603             return;\r
604         modifyListeners.remove(listener);\r
605     }\r
606 \r
607     public void setInputValidator(IInputValidator validator) {\r
608         if (validator != this.validator) {\r
609             this.validator = validator;\r
610         }\r
611     }\r
612 \r
613     private String isTextValid() {\r
614         if (validator != null) {\r
615                 String result = validator.isValid(getWidget().getText());\r
616                 for(Consumer<String> listener : validationListeners) listener.accept(result);\r
617             return result;\r
618         }\r
619         return null;\r
620     }\r
621 \r
622     public void setColorProvider(ITrackedColorProvider provider) {\r
623         Assert.isNotNull(provider);\r
624         this.colorProvider = provider;\r
625     }\r
626 \r
627     private void setBackground(Color background) {\r
628         if(text.isDisposed()) return;\r
629         if (!text.getEditable()) {\r
630             // Do not alter background when the widget is not editable.\r
631             return;\r
632         }\r
633         text.setBackground(background);\r
634     }\r
635     \r
636     public boolean isDisposed() {\r
637         return text.isDisposed();\r
638     }\r
639     \r
640     public Display getDisplay() {\r
641         return display;\r
642     }\r
643     \r
644     public void addValidationListener(Consumer<String> listener) {\r
645         validationListeners.add(listener);\r
646     }\r
647 \r
648     public void removeValidationListener(Consumer<String> listener) {\r
649         validationListeners.remove(listener);\r
650     }\r
651     \r
652     public String getText() {\r
653                 return text.getText();\r
654         }\r
655     \r
656     public int getCaretPosition() {\r
657         return text.getCaretPosition();\r
658     }\r
659     \r
660     public void undo() {\r
661         if (undoStack.size() > 0) {\r
662             String lastEdit = undoStack.remove(0);\r
663             if (lastEdit.equals(text.getText().trim())) {\r
664                 if (undoStack.size() == 0)\r
665                     return;\r
666                 lastEdit = undoStack.remove(0);\r
667             }\r
668             String currText = text.getText();\r
669             textBeforeEdit = currText;\r
670             text.setText(lastEdit);\r
671             moveCursorToEnd();\r
672             redoStack.add(0, currText);\r
673         }\r
674     }\r
675 \r
676     public void redo() {\r
677         if (redoStack.size() > 0) {\r
678             String text = (String) redoStack.remove(0);\r
679             moveCursorToEnd();\r
680             String currText = this.text.getText();\r
681             addToUndoStack(currText);\r
682             textBeforeEdit = currText;\r
683             this.text.setText(text);\r
684             moveCursorToEnd();\r
685         }\r
686     }\r
687 \r
688     private void moveCursorToEnd() {\r
689         text.setSelection(text.getText().length());\r
690     }\r
691 }\r