1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 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.utils.ui.widgets;
\r
14 import org.eclipse.core.runtime.Assert;
\r
15 import org.eclipse.core.runtime.ListenerList;
\r
16 import org.eclipse.jface.dialogs.IInputValidator;
\r
17 import org.eclipse.jface.resource.ColorDescriptor;
\r
18 import org.eclipse.jface.resource.JFaceResources;
\r
19 import org.eclipse.jface.resource.LocalResourceManager;
\r
20 import org.eclipse.jface.resource.ResourceManager;
\r
21 import org.eclipse.swt.SWT;
\r
22 import org.eclipse.swt.events.DisposeEvent;
\r
23 import org.eclipse.swt.events.DisposeListener;
\r
24 import org.eclipse.swt.events.FocusEvent;
\r
25 import org.eclipse.swt.events.FocusListener;
\r
26 import org.eclipse.swt.events.KeyEvent;
\r
27 import org.eclipse.swt.events.KeyListener;
\r
28 import org.eclipse.swt.events.ModifyEvent;
\r
29 import org.eclipse.swt.events.ModifyListener;
\r
30 import org.eclipse.swt.events.MouseEvent;
\r
31 import org.eclipse.swt.events.MouseListener;
\r
32 import org.eclipse.swt.events.MouseTrackListener;
\r
33 import org.eclipse.swt.graphics.Color;
\r
34 import org.eclipse.swt.graphics.RGB;
\r
35 import org.eclipse.swt.widgets.Composite;
\r
36 import org.eclipse.swt.widgets.Text;
\r
39 * This is a TrackedTest SWT Text-widget 'decorator'.
\r
41 * The widget has 2 main states: editing and inactive.
\r
43 * It implements the necessary listeners to achieve the text widget behaviour
\r
44 * needed by Simantics. User notification about modifications is provided via
\r
45 * {@link TrackedModifyListener}.
\r
50 * // #1: create new Text internally, use TrackedModifylistener
\r
51 * TrackedText trackedText = new TrackedText(parentComposite, style);
\r
52 * trackedText.addModifyListener(new TrackedModifyListener() {
\r
53 * public void modifyText(TrackedModifyEvent e) {
\r
54 * // text was modified, do something.
\r
58 * // #2: create new Text internally, define the colors for text states.
\r
59 * TrackedText trackedText = new TrackedText(text, <instance of ITrackedColorProvider>);
\r
62 * @author Tuukka Lehtonen
\r
64 public class TrackedText {
\r
66 public static final String TRACKED_TEXT_KEY = "TrackedTextKey";
\r
68 private static final int EDITING = 1 << 0;
\r
69 private static final int MODIFIED_DURING_EDITING = 1 << 1;
\r
72 * Used to tell whether or not a mouseDown has occurred after a focusGained
\r
73 * event to be able to select the whole text field when it is pressed for
\r
74 * the first time while the widget holds focus.
\r
76 private static final int MOUSE_DOWN_FIRST_TIME = 1 << 2;
\r
77 private static final int MOUSE_INSIDE_CONTROL = 1 << 3;
\r
81 private int caretPositionBeforeEdit;
\r
83 private String textBeforeEdit;
\r
85 private final Text text;
\r
87 private CompositeListener listener;
\r
89 private ListenerList modifyListeners;
\r
91 private IInputValidator validator;
\r
93 private ITrackedColorProvider colorProvider;
\r
95 private ResourceManager resourceManager;
\r
97 private class DefaultColorProvider implements ITrackedColorProvider {
\r
98 private final ColorDescriptor highlightColor = ColorDescriptor.createFrom(new RGB(254, 255, 197));
\r
99 private final ColorDescriptor inactiveColor = ColorDescriptor.createFrom(new RGB(245, 246, 190));
\r
100 private final ColorDescriptor invalidInputColor = ColorDescriptor.createFrom(new RGB(255, 128, 128));
\r
103 public Color getEditingBackground() {
\r
108 public Color getHoverBackground() {
\r
109 return resourceManager.createColor(highlightColor);
\r
113 public Color getInactiveBackground() {
\r
114 return resourceManager.createColor(inactiveColor);
\r
118 public Color getInvalidBackground() {
\r
119 return resourceManager.createColor(invalidInputColor);
\r
124 * A composite of many UI listeners for creating the functionality of this
\r
127 private class CompositeListener
\r
128 implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener,
\r
129 MouseListener, FocusListener
\r
131 // Keyboard/editing events come in the following order:
\r
137 public void modifyText(ModifyEvent e) {
\r
138 //System.out.println("modifyText: " + e);
\r
141 String valid = isTextValid();
\r
142 if (valid != null) {
\r
143 setBackground(colorProvider.getInvalidBackground());
\r
146 setBackground(colorProvider.getEditingBackground());
\r
148 setBackground(colorProvider.getInactiveBackground());
\r
152 public void widgetDisposed(DisposeEvent e) {
\r
153 getWidget().removeModifyListener(this);
\r
156 private boolean isMultiLine() {
\r
157 return (text.getStyle() & SWT.MULTI) != 0;
\r
160 private boolean hasMultiLineCommitModifier(KeyEvent e) {
\r
161 return (e.stateMask & SWT.CTRL) != 0;
\r
164 public void keyPressed(KeyEvent e) {
\r
165 //System.out.println("keyPressed: " + e);
\r
166 if (!isEditing()) {
\r
167 // ESC, ENTER & keypad ENTER must not start editing
\r
168 if (e.keyCode == SWT.ESC)
\r
171 if (!isMultiLine()) {
\r
172 if (e.keyCode == SWT.F2 || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
174 } else if (e.character != '\0') {
\r
178 // In multi-line mode, TAB must not start editing!
\r
179 if (e.keyCode == SWT.F2) {
\r
181 } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
182 if (hasMultiLineCommitModifier(e)) {
\r
187 } else if (e.keyCode == SWT.TAB) {
\r
188 text.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT);
\r
190 } else if (e.character != '\0') {
\r
195 // ESC reverts any changes made during this edit
\r
196 if (e.keyCode == SWT.ESC) {
\r
199 if (!isMultiLine()) {
\r
200 if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
204 if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
\r
205 if (hasMultiLineCommitModifier(e)) {
\r
214 public void keyReleased(KeyEvent e) {
\r
215 //System.out.println("keyReleased: " + e);
\r
218 public void mouseEnter(MouseEvent e) {
\r
219 //System.out.println("mouseEnter");
\r
220 if (!isEditing()) {
\r
221 setBackground(colorProvider.getHoverBackground());
\r
223 setMouseInsideControl(true);
\r
226 public void mouseExit(MouseEvent e) {
\r
227 //System.out.println("mouseExit");
\r
228 if (!isEditing()) {
\r
229 setBackground(colorProvider.getInactiveBackground());
\r
231 setMouseInsideControl(false);
\r
234 public void mouseHover(MouseEvent e) {
\r
235 //System.out.println("mouseHover");
\r
236 setMouseInsideControl(true);
\r
239 public void mouseDoubleClick(MouseEvent e) {
\r
240 //System.out.println("mouseDoubleClick: " + e);
\r
241 if (e.button == 1) {
\r
242 getWidget().selectAll();
\r
246 public void mouseDown(MouseEvent e) {
\r
247 //System.out.println("mouseDown: " + e);
\r
248 if (!isEditing()) {
\r
249 // In reality we should never get here, since focusGained
\r
250 // always comes before mouseDown, but let's keep this
\r
251 // fallback just to be safe.
\r
252 if (e.button == 1) {
\r
256 if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) {
\r
257 getWidget().selectAll();
\r
258 state &= ~MOUSE_DOWN_FIRST_TIME;
\r
263 public void mouseUp(MouseEvent e) {
\r
266 public void focusGained(FocusEvent e) {
\r
267 //System.out.println("focusGained");
\r
268 if (!isEditing()) {
\r
269 if (!isMultiLine()) {
\r
270 // Always start edit on single line texts when focus is gained
\r
276 public void focusLost(FocusEvent e) {
\r
277 //System.out.println("focusLost");
\r
284 public TrackedText(Text text) {
\r
285 Assert.isNotNull(text);
\r
288 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);
\r
289 this.colorProvider = new DefaultColorProvider();
\r
294 public TrackedText(Composite parent, int style) {
\r
296 this.text = new Text(parent, style);
\r
297 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text);
\r
298 this.colorProvider = new DefaultColorProvider();
\r
303 public TrackedText(Text text, ITrackedColorProvider colorProvider) {
\r
304 Assert.isNotNull(text, "text must not be null");
\r
305 Assert.isNotNull(colorProvider, "colorProvider must not be null");
\r
308 this.colorProvider = colorProvider;
\r
313 public TrackedText(Composite parent, int style, ITrackedColorProvider colorProvider) {
\r
314 Assert.isNotNull(colorProvider, "colorProvider must not be null");
\r
316 this.text = new Text(parent, style);
\r
317 this.colorProvider = colorProvider;
\r
323 * Common initialization. Assumes that text is already created.
\r
325 private void initialize() {
\r
326 Assert.isNotNull(text);
\r
328 text.setBackground(colorProvider.getInactiveBackground());
\r
329 text.setDoubleClickEnabled(false);
\r
330 text.setData(TRACKED_TEXT_KEY, this);
\r
332 listener = new CompositeListener();
\r
334 text.addModifyListener(listener);
\r
335 text.addDisposeListener(listener);
\r
336 text.addKeyListener(listener);
\r
337 text.addMouseTrackListener(listener);
\r
338 text.addMouseListener(listener);
\r
339 text.addFocusListener(listener);
\r
342 private void startEdit(boolean selectAll) {
\r
344 // Print some debug incase we end are in an invalid state
\r
345 System.out.println("TrackedText: BUG: startEdit called when in editing state");
\r
347 //System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit);
\r
349 // Backup text-field data for reverting purposes
\r
350 caretPositionBeforeEdit = text.getCaretPosition();
\r
351 textBeforeEdit = text.getText();
\r
353 // Signal editing state
\r
354 setBackground(colorProvider.getEditingBackground());
\r
359 state |= EDITING | MOUSE_DOWN_FIRST_TIME;
\r
362 private void applyEdit() {
\r
364 if (isTextValid() != null) {
\r
365 text.setText(textBeforeEdit);
\r
366 } else if (isModified() && !text.getText().equals(textBeforeEdit)) {
\r
367 //System.out.println("apply");
\r
368 if (modifyListeners != null) {
\r
369 TrackedModifyEvent event = new TrackedModifyEvent(text, text.getText());
\r
370 for (Object o : modifyListeners.getListeners()) {
\r
371 ((TrackedModifyListener) o).modifyText(event);
\r
380 private void endEdit() {
\r
381 if (!isEditing()) {
\r
382 // Print some debug incase we end are in an invalid state
\r
383 //ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state"));
\r
385 setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
\r
386 //System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection());
\r
387 // Always move the caret to the end of the string
\r
388 text.setSelection(text.getCharCount());
\r
389 state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);
\r
390 setModified(false);
\r
393 private void revertEdit() {
\r
394 if (!isEditing()) {
\r
395 // Print some debug incase we end are in an invalid state
\r
396 //ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state"));
\r
397 System.out.println("BUG: revertEdit called when not in editing state");
\r
399 text.setText(textBeforeEdit);
\r
400 text.setSelection(caretPositionBeforeEdit);
\r
401 setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
\r
402 state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME);
\r
403 setModified(false);
\r
406 public boolean isEditing() {
\r
407 return (state & EDITING) != 0;
\r
410 private void setModified(boolean modified) {
\r
412 state |= MODIFIED_DURING_EDITING;
\r
414 state &= ~MODIFIED_DURING_EDITING;
\r
418 private boolean isMouseInsideControl() {
\r
419 return (state & MOUSE_INSIDE_CONTROL) != 0;
\r
422 private void setMouseInsideControl(boolean inside) {
\r
424 state |= MOUSE_INSIDE_CONTROL;
\r
426 state &= ~MOUSE_INSIDE_CONTROL;
\r
429 public boolean isModified() {
\r
430 return (state & MODIFIED_DURING_EDITING) != 0;
\r
433 public void setEditable(boolean editable) {
\r
435 text.setEditable(true);
\r
436 setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
\r
438 text.setEditable(false);
\r
439 text.setBackground(null);
\r
443 public void setText(String text) {
\r
444 this.text.setText(text);
\r
447 public void setTextWithoutNotify(String text) {
\r
448 this.text.removeModifyListener(listener);
\r
450 this.text.addModifyListener(listener);
\r
453 public Text getWidget() {
\r
457 public synchronized void addModifyListener(TrackedModifyListener listener) {
\r
458 if (modifyListeners == null) {
\r
459 modifyListeners = new ListenerList(ListenerList.IDENTITY);
\r
461 modifyListeners.add(listener);
\r
464 public synchronized void removeModifyListener(TrackedModifyListener listener) {
\r
465 if (modifyListeners == null)
\r
467 modifyListeners.remove(listener);
\r
470 public void setInputValidator(IInputValidator validator) {
\r
471 if (validator != this.validator) {
\r
472 this.validator = validator;
\r
476 private String isTextValid() {
\r
477 if (validator != null) {
\r
478 return validator.isValid(getWidget().getText());
\r
483 public void setColorProvider(ITrackedColorProvider provider) {
\r
484 Assert.isNotNull(provider);
\r
485 this.colorProvider = provider;
\r
488 public void updateColor() {
\r
490 // Editing logic should take care of setting the appropriate
\r
491 // background color.
\r
492 } else if (!text.getEditable()) {
\r
493 text.setBackground(null);
\r
495 setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground());
\r
499 private void setBackground(Color background) {
\r
500 if (!text.getEditable() || (background != null && background.isDisposed())) {
\r
501 // Do not alter background when the widget is not editable.
\r
504 text.setBackground(background);
\r