/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.utils.ui.widgets; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.resource.ColorDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CaretEvent; import org.eclipse.swt.custom.CaretListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a TrackedTest SWT Text-widget 'decorator'. * * The widget has 2 main states: editing and inactive. * * It implements the necessary listeners to achieve the text widget behaviour * needed by Simantics. User notification about modifications is provided via * {@link TrackedModifyListener}. * * Examples: * *
 * // #1: create new Text internally, use TrackedModifylistener
 * TrackedText trackedText = new TrackedText(parentComposite, style);
 * trackedText.addModifyListener(new TrackedModifyListener() {
 *     public void modifyText(TrackedModifyEvent e) {
 *         // text was modified, do something.
 *     }
 * });
 * 
 * // #2: create new Text internally, define the colors for text states.
 * TrackedText trackedText = new TrackedText(text, <instance of ITrackedColorProvider>);
 * 
* * @author Tuukka Lehtonen */ public class TrackedStyledText { private static final Logger LOGGER = LoggerFactory.getLogger(TrackedStyledText.class); private static final int EDITING = 1 << 0; private static final int MODIFIED_DURING_EDITING = 1 << 1; /** * Used to tell whether or not a mouseDown has occurred after a focusGained * event to be able to select the whole text field when it is pressed for * the first time while the widget holds focus. */ private static final int MOUSE_DOWN_FIRST_TIME = 1 << 2; private static final int MOUSE_INSIDE_CONTROL = 1 << 3; private int state; private int caretPositionBeforeEdit; private String textBeforeEdit; private final StyledText text; private CompositeListener listener; private ListenerList caretListeners; private ListenerList modifyListeners; private IInputValidator validator; private ITrackedColorProvider colorProvider; private ResourceManager resourceManager; private class DefaultColorProvider implements ITrackedColorProvider { private final ColorDescriptor highlightColor = ColorDescriptor.createFrom(new RGB(254, 255, 197)); private final ColorDescriptor inactiveColor = ColorDescriptor.createFrom(new RGB(245, 246, 190)); private final ColorDescriptor invalidInputColor = ColorDescriptor.createFrom(new RGB(255, 128, 128)); @Override public Color getEditingBackground() { return null; } @Override public Color getHoverBackground() { return resourceManager.createColor(highlightColor); } @Override public Color getInactiveBackground() { return resourceManager.createColor(inactiveColor); } @Override public Color getInvalidBackground() { return resourceManager.createColor(invalidInputColor); } }; /** * A composite of many UI listeners for creating the functionality of this * class. */ private class CompositeListener implements ModifyListener, DisposeListener, KeyListener, MouseTrackListener, MouseListener, FocusListener, CaretListener { // Keyboard/editing events come in the following order: // 1. keyPressed // 2. verifyText // 3. modifyText // 4. keyReleased public void modifyText(ModifyEvent e) { //System.out.println("modifyText: " + e); setModified(true); String valid = isTextValid(); if (valid != null) { setBackground(colorProvider.getInvalidBackground()); } else { if (isEditing()) setBackground(colorProvider.getEditingBackground()); else setBackground(colorProvider.getInactiveBackground()); } } public void widgetDisposed(DisposeEvent e) { getWidget().removeModifyListener(this); } private boolean isMultiLine() { return (text.getStyle() & SWT.MULTI) != 0; } private boolean hasMultiLineCommitModifier(KeyEvent e) { return (e.stateMask & SWT.CTRL) != 0; } public void keyPressed(KeyEvent e) { //System.out.println("keyPressed: " + e); if (!isEditing()) { // ESC, ENTER & keypad ENTER must not start editing if (e.keyCode == SWT.ESC) return; if (!isMultiLine()) { if (e.keyCode == SWT.F2 || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { startEdit(true); } else if (e.character != '\0') { startEdit(false); } } else { // In multi-line mode, TAB must not start editing! if (e.keyCode == SWT.F2) { startEdit(true); } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { if (hasMultiLineCommitModifier(e)) { e.doit = false; } else { startEdit(false); } } else if (e.keyCode == SWT.TAB) { text.traverse(((e.stateMask & SWT.SHIFT) != 0) ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT); e.doit = false; } else if (e.character != '\0') { startEdit(false); } } } else { // ESC reverts any changes made during this edit if (e.keyCode == SWT.ESC) { revertEdit(); } if (!isMultiLine()) { if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { applyEdit(); } } else { if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { if (hasMultiLineCommitModifier(e)) { applyEdit(); e.doit = false; } } } } } public void keyReleased(KeyEvent e) { //System.out.println("keyReleased: " + e); } public void mouseEnter(MouseEvent e) { //System.out.println("mouseEnter"); if (!isEditing()) { setBackground(colorProvider.getHoverBackground()); } setMouseInsideControl(true); } public void mouseExit(MouseEvent e) { //System.out.println("mouseExit"); if (!isEditing()) { setBackground(colorProvider.getInactiveBackground()); } setMouseInsideControl(false); } public void mouseHover(MouseEvent e) { //System.out.println("mouseHover"); setMouseInsideControl(true); } public void mouseDoubleClick(MouseEvent e) { //System.out.println("mouseDoubleClick: " + e); if (e.button == 1) { getWidget().selectAll(); } } public void mouseDown(MouseEvent e) { //System.out.println("mouseDown: " + e); if (!isEditing()) { // In reality we should never get here, since focusGained // always comes before mouseDown, but let's keep this // fallback just to be safe. if (e.button == 1) { startEdit(false); } } else { if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) { getWidget().selectAll(); state &= ~MOUSE_DOWN_FIRST_TIME; } } } public void mouseUp(MouseEvent e) { } public void focusGained(FocusEvent e) { //System.out.println("focusGained"); if (!isEditing()) { if (!isMultiLine()) { // Always start edit on single line texts when focus is gained startEdit(true); } } } public void focusLost(FocusEvent e) { //System.out.println("focusLost"); if (isEditing()) { applyEdit(); } } @Override public void caretMoved(CaretEvent event) { fireCaretListeners(); } } public TrackedStyledText(StyledText text) { Assert.isNotNull(text); this.state = 0; this.text = text; this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text); this.colorProvider = new DefaultColorProvider(); initialize(); } public TrackedStyledText(Composite parent, int style) { this.state = 0; this.text = new StyledText(parent, style); this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), text); this.colorProvider = new DefaultColorProvider(); initialize(); } public TrackedStyledText(StyledText text, ITrackedColorProvider colorProvider) { Assert.isNotNull(text, "text must not be null"); Assert.isNotNull(colorProvider, "colorProvider must not be null"); this.state = 0; this.text = text; this.colorProvider = colorProvider; initialize(); } public TrackedStyledText(Composite parent, int style, ITrackedColorProvider colorProvider) { Assert.isNotNull(colorProvider, "colorProvider must not be null"); this.state = 0; this.text = new StyledText(parent, style); this.colorProvider = colorProvider; initialize(); } /** * Common initialization. Assumes that text is already created. */ private void initialize() { Assert.isNotNull(text); text.setBackground(colorProvider.getInactiveBackground()); text.setDoubleClickEnabled(false); listener = new CompositeListener(); text.addModifyListener(listener); text.addDisposeListener(listener); text.addKeyListener(listener); text.addMouseTrackListener(listener); text.addMouseListener(listener); text.addFocusListener(listener); text.addCaretListener(listener); } private void startEdit(boolean selectAll) { if (isEditing()) { // Print some debug incase we end are in an invalid state System.out.println("TrackedText: BUG: startEdit called when in editing state"); } System.out.println("start edit: selectall=" + selectAll + ", text=" + text.getText() + ", caretpos=" + caretPositionBeforeEdit); // Backup text-field data for reverting purposes caretPositionBeforeEdit = text.getCaretOffset(); textBeforeEdit = text.getText(); // Signal editing state setBackground(colorProvider.getEditingBackground()); if (selectAll) { text.selectAll(); } state |= EDITING | MOUSE_DOWN_FIRST_TIME; } private void applyEdit() { LOGGER.info("apply edit"); try { if (isTextValid() != null) { text.setText(textBeforeEdit); } else if (isModified() && !text.getText().equals(textBeforeEdit)) { //System.out.println("apply"); if (modifyListeners != null) { TrackedModifyEvent event = new TrackedModifyEvent(text, text.getText()); for (Object o : modifyListeners.getListeners()) { ((TrackedModifyListener) o).modifyText(event); } } } } finally { endEdit(); } } private void fireCaretListeners() { if (caretListeners != null) { for (Object o : caretListeners.getListeners()) { ((TrackedCaretListener) o).caretOrSelectionChanged(); } } } private void endEdit() { LOGGER.info("endedit"); if (!isEditing()) { // Print some debug incase we end are in an invalid state //ExceptionUtils.logError(new Exception("BUG: endEdit called when not in editing state")); System.out.println(); } setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground()); //System.out.println("endEdit: " + text.getText() + ", caret: " + text.getCaretLocation() + ", selection: " + text.getSelection()); // Always move the caret to the end of the string //text.setSelection(text.getCharCount()); state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME); setModified(false); } private void revertEdit() { if (!isEditing()) { // Print some debug incase we end are in an invalid state //ExceptionUtils.logError(new Exception("BUG: revertEdit called when not in editing state")); LOGGER.warn("BUG: revertEdit called when not in editing state"); } text.setText(textBeforeEdit); text.setSelection(caretPositionBeforeEdit); setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground()); state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME); setModified(false); } public boolean isEditing() { return (state & EDITING) != 0; } private void setModified(boolean modified) { if (modified) { state |= MODIFIED_DURING_EDITING; } else { state &= ~MODIFIED_DURING_EDITING; } } private boolean isMouseInsideControl() { return (state & MOUSE_INSIDE_CONTROL) != 0; } private void setMouseInsideControl(boolean inside) { if (inside) state |= MOUSE_INSIDE_CONTROL; else state &= ~MOUSE_INSIDE_CONTROL; } public boolean isModified() { return (state & MODIFIED_DURING_EDITING) != 0; } public void setEditable(boolean editable) { if (editable) { text.setEditable(true); setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground()); } else { text.setEditable(false); text.setBackground(null); } } public void setText(String text) { this.text.setText(text); } public void setTextWithoutNotify(String text) { this.text.removeModifyListener(listener); setText(text); this.text.addModifyListener(listener); } public StyledText getWidget() { return text; } public synchronized void addModifyListener(TrackedModifyListener listener) { if (modifyListeners == null) { modifyListeners = new ListenerList(ListenerList.IDENTITY); } modifyListeners.add(listener); } public synchronized void addCaretListener(TrackedCaretListener listener) { if (caretListeners == null) { caretListeners = new ListenerList(ListenerList.IDENTITY); } caretListeners.add(listener); } public synchronized void removeModifyListener(TrackedModifyListener listener) { if (modifyListeners == null) return; modifyListeners.remove(listener); } public synchronized void removeCaretListener(TrackedCaretListener listener) { if (caretListeners == null) return; caretListeners.remove(listener); } public void setInputValidator(IInputValidator validator) { if (validator != this.validator) { this.validator = validator; } } private String isTextValid() { if (validator != null) { return validator.isValid(getWidget().getText()); } return null; } public void setColorProvider(ITrackedColorProvider provider) { Assert.isNotNull(provider); this.colorProvider = provider; } private void setBackground(Color background) { if (!text.getEditable()) { // Do not alter background when the widget is not editable. return; } text.setBackground(background); } }