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
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.browsing.ui.swt.widgets;
\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
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
57 * This is a TrackedTest SWT Text-widget 'decorator'.
\r
59 * The widget has 2 main states: editing and inactive.
\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
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
76 * // #2: create new Text internally, define the colors for text states.
\r
77 * TrackedText trackedText = new TrackedText(text, <instance of ITrackedColorProvider>);
\r
80 * @author Tuukka Lehtonen
\r
82 public class TrackedText implements Widget {
\r
84 public static final String ID = "TRACKED_TEXT";
\r
86 private static final int EDITING = 1 << 0;
\r
87 private static final int MODIFIED_DURING_EDITING = 1 << 1;
\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
94 private static final int MOUSE_DOWN_FIRST_TIME = 1 << 2;
\r
95 private static final int MOUSE_INSIDE_CONTROL = 1 << 3;
\r
99 private int caretPositionBeforeEdit;
\r
101 private String textBeforeEdit;
\r
103 private final Display display;
\r
105 private final Text text;
\r
107 private CompositeListener listener;
\r
109 private ListenerList modifyListeners;
\r
111 private IInputValidator validator;
\r
113 private ITrackedColorProvider colorProvider;
\r
115 private final ResourceManager resourceManager;
\r
117 private ReadFactory<?, String> textFactory;
\r
119 private boolean moveCaretAfterEdit = true;
\r
121 private boolean selectAllOnStartEdit = true;
\r
123 private final CopyOnWriteArrayList<Consumer<String>> validationListeners = new CopyOnWriteArrayList<>();
\r
126 // UNDO REDO HANDLER
\r
128 private static final int MAX_STACK_SIZE = 25;
\r
130 private List<String> undoStack = new LinkedList<String>();
\r
131 private List<String> redoStack = new LinkedList<String>();
\r
133 public void setTextFactory(ReadFactory<?, String> textFactory) {
\r
134 this.textFactory = textFactory;
\r
137 public void setFont(Font font) {
\r
138 text.setFont(font);
\r
141 public void setMoveCaretAfterEdit(boolean value) {
\r
142 this.moveCaretAfterEdit = value;
\r
146 public void setInput(ISessionContext context, Object input) {
\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
156 if(textFactory != null) {
\r
157 textFactory.listen(context, input, new Listener<String>() {
\r
160 public void exception(final Throwable t) {
\r
161 display.asyncExec(new Runnable() {
\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
174 public void execute(final String string) {
\r
176 if(text.isDisposed()) return;
\r
178 display.asyncExec(new Runnable() {
\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
192 public boolean isDisposed() {
\r
193 return text.isDisposed();
\r
202 * A composite of many UI listeners for creating the functionality of this
\r
205 private class CompositeListener
\r
206 implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,
\r
207 MouseListener, FocusListener
\r
209 // Keyboard/editing events come in the following order:
\r
216 public void modifyText(ModifyEvent e) {
\r
217 //System.out.println("modifyText: " + e);
\r
220 String valid = isTextValid();
\r
221 if (valid != null) {
\r
222 setBackground(colorProvider.getInvalidBackground());
\r
225 setBackground(colorProvider.getEditingBackground());
\r
227 setBackground(colorProvider.getInactiveBackground());
\r
232 public void widgetDisposed(DisposeEvent e) {
\r
233 getWidget().removeModifyListener(this);
\r
236 private boolean isMultiLine() {
\r
237 return (text.getStyle() & SWT.MULTI) != 0;
\r
240 private boolean hasMultiLineCommitModifier(KeyEvent e) {
\r
241 return (e.stateMask & SWT.CTRL) != 0;
\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
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
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
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
271 } else if (e.character != '\0') {
\r
276 // ESC reverts any changes made during this edit
\r
277 if (e.keyCode == SWT.ESC) {
\r
280 if (!isMultiLine()) {
\r
281 if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
285 if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
286 if (hasMultiLineCommitModifier(e)) {
\r
296 public void keyReleased(KeyEvent e) {
\r
297 //System.out.println("keyReleased: " + e);
\r
301 public void mouseEnter(MouseEvent e) {
\r
302 //System.out.println("mouseEnter");
\r
303 if (!isEditing()) {
\r
304 setBackground(colorProvider.getHoverBackground());
\r
306 setMouseInsideControl(true);
\r
310 public void mouseExit(MouseEvent e) {
\r
311 //System.out.println("mouseExit");
\r
312 if (!isEditing()) {
\r
313 setBackground(colorProvider.getInactiveBackground());
\r
315 setMouseInsideControl(false);
\r
319 public void mouseHover(MouseEvent e) {
\r
320 //System.out.println("mouseHover");
\r
321 setMouseInsideControl(true);
\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
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
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
348 state &= ~MOUSE_DOWN_FIRST_TIME;
\r
354 public void mouseUp(MouseEvent e) {
\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
369 public void focusLost(FocusEvent e) {
\r
370 //System.out.println("focusLost");
\r
377 public TrackedText(Composite parent, WidgetSupport support, int style) {
\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
387 createUndoRedoHandler();
\r
390 private void createUndoRedoHandler() {
\r
392 text.addModifyListener(new ModifyListener() {
\r
394 private int eventTimeOut = 1000;
\r
395 private long lastEventTimeStamp = 0;
\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
405 addToUndoStack(newText);
\r
408 lastEventTimeStamp = (event.time & 0xFFFFFFFFL);
\r
412 text.addFocusListener(new FocusListener() {
\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
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
433 public ResourceManager getResourceManager() {
\r
434 return resourceManager;
\r
438 * Common initialization. Assumes that text is already created.
\r
440 private void initialize() {
\r
441 Assert.isNotNull(text);
\r
443 text.setBackground(colorProvider.getInactiveBackground());
\r
444 text.setDoubleClickEnabled(false);
\r
446 listener = new CompositeListener();
\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
456 public void startEdit(boolean selectAll) {
\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
461 // System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);
\r
463 // Backup text-field data for reverting purposes
\r
464 caretPositionBeforeEdit = text.getCaretPosition();
\r
465 textBeforeEdit = text.getText();
\r
467 // Signal editing state
\r
468 setBackground(colorProvider.getEditingBackground());
\r
473 state |= EDITING | MOUSE_DOWN_FIRST_TIME;
\r
476 private void applyEdit() {
\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
490 } catch (Throwable t) {
\r
491 t.printStackTrace();
\r
497 private void endEdit() {
\r
498 if (text.isDisposed())
\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
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
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
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
528 private boolean isEditing() {
\r
529 return (state & EDITING) != 0;
\r
532 private void setModified(boolean modified) {
\r
534 state |= MODIFIED_DURING_EDITING;
\r
536 state &= ~MODIFIED_DURING_EDITING;
\r
540 private boolean isMouseInsideControl() {
\r
541 return (state & MOUSE_INSIDE_CONTROL) != 0;
\r
544 private void setMouseInsideControl(boolean inside) {
\r
546 state |= MOUSE_INSIDE_CONTROL;
\r
548 state &= ~MOUSE_INSIDE_CONTROL;
\r
551 private boolean isModified() {
\r
552 return (state & MODIFIED_DURING_EDITING) != 0;
\r
555 public void setSelectAllOnStartEdit(boolean selectAll) {
\r
556 this.selectAllOnStartEdit = selectAll;
\r
559 public void setEditable(boolean editable) {
\r
561 text.setEditable(true);
\r
562 setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
\r
564 text.setEditable(false);
\r
565 text.setBackground(null);
\r
569 public void setText(String text) {
\r
570 this.text.setText(text);
\r
571 addToUndoStack(text);
\r
574 private void addToUndoStack(String text) {
\r
575 if (isTextValid() != null)
\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
584 public void setTextWithoutNotify(String text) {
\r
585 this.text.removeModifyListener(listener);
\r
587 this.text.addModifyListener(listener);
\r
590 public Text getWidget() {
\r
594 public synchronized void addModifyListener(TextModifyListener listener) {
\r
595 if (modifyListeners == null) {
\r
596 modifyListeners = new ListenerList(ListenerList.IDENTITY);
\r
598 modifyListeners.add(listener);
\r
601 public synchronized void removeModifyListener(TextModifyListener listener) {
\r
602 if (modifyListeners == null)
\r
604 modifyListeners.remove(listener);
\r
607 public void setInputValidator(IInputValidator validator) {
\r
608 if (validator != this.validator) {
\r
609 this.validator = validator;
\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
622 public void setColorProvider(ITrackedColorProvider provider) {
\r
623 Assert.isNotNull(provider);
\r
624 this.colorProvider = provider;
\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
633 text.setBackground(background);
\r
636 public boolean isDisposed() {
\r
637 return text.isDisposed();
\r
640 public Display getDisplay() {
\r
644 public void addValidationListener(Consumer<String> listener) {
\r
645 validationListeners.add(listener);
\r
648 public void removeValidationListener(Consumer<String> listener) {
\r
649 validationListeners.remove(listener);
\r
652 public String getText() {
\r
653 return text.getText();
\r
656 public int getCaretPosition() {
\r
657 return text.getCaretPosition();
\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
666 lastEdit = undoStack.remove(0);
\r
668 String currText = text.getText();
\r
669 textBeforeEdit = currText;
\r
670 text.setText(lastEdit);
\r
672 redoStack.add(0, currText);
\r
676 public void redo() {
\r
677 if (redoStack.size() > 0) {
\r
678 String text = (String) redoStack.remove(0);
\r
680 String currText = this.text.getText();
\r
681 addToUndoStack(currText);
\r
682 textBeforeEdit = currText;
\r
683 this.text.setText(text);
\r
688 private void moveCursorToEnd() {
\r
689 text.setSelection(text.getText().length());
\r