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