/******************************************************************************* * Copyright (c) 2007, 2012 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.browsing.ui.swt.widgets; import java.util.Map; 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.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.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.simantics.browsing.ui.swt.widgets.impl.ITrackedColorProvider; import org.simantics.browsing.ui.swt.widgets.impl.ReadFactory; import org.simantics.browsing.ui.swt.widgets.impl.TextModifyListener; import org.simantics.browsing.ui.swt.widgets.impl.TrackedModifyEvent; import org.simantics.browsing.ui.swt.widgets.impl.Widget; import org.simantics.browsing.ui.swt.widgets.impl.WidgetSupport; import org.simantics.db.management.ISessionContext; import org.simantics.db.procedure.Listener; import org.simantics.utils.threads.SWTThread; public class TrackedCombo implements Widget { 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; final private org.eclipse.swt.widgets.Combo combo; private CompositeListener listener; private ListenerList modifyListeners; private IInputValidator validator; private ITrackedColorProvider colorProvider; private final ResourceManager resourceManager; private ReadFactory> itemFactory; protected ReadFactory selectionFactory; public void setItemFactory(ReadFactory> itemFactory) { this.itemFactory = itemFactory; } public void setSelectionFactory(ReadFactory selectionFactory) { this.selectionFactory = selectionFactory; } public void setFont(Font font) { combo.setFont(font); } @Override public void setInput(ISessionContext context, Object input) { if (modifyListeners != null) { for (Object listener : modifyListeners.getListeners()) { if(listener instanceof Widget) { ((Widget) listener).setInput(context, input); } } } if(itemFactory != null) { itemFactory.listen(context, input, new Listener>() { @Override public void exception(Throwable t) { t.printStackTrace(); } @Override public void execute(final Map items) { if(isDisposed()) return; Runnable r = new Runnable() { @Override public void run() { if(isDisposed()) return; // System.out.println("Combo received new items: " + items.size()); // if(modifyListeners != null) // for(Object listener : modifyListeners.getListeners()) combo.removeModifyListener((ModifyListener)listener); if(listener != null) combo.removeModifyListener(listener); combo.setData(items); combo.clearSelection(); try { combo.removeAll(); } catch (Throwable t) { t.printStackTrace(); } int index = 0; for(String key : items.keySet()) { // System.out.println("-" + key); combo.add(key); combo.setData(key, index++); } String selectionKey = (String)combo.getData("_SelectionKey"); if(selectionKey != null) { Integer selectionIndex = (Integer)combo.getData(selectionKey); if(selectionIndex != null) combo.select(selectionIndex); } // if(modifyListeners != null) // for(Object listener : modifyListeners.getListeners()) combo.addModifyListener((ModifyListener)listener); if(listener != null) combo.addModifyListener(listener); //label.setSize(200, 20); // label.getParent().layout(); // label.getParent().getParent().layout(); } }; if(SWTThread.getThreadAccess().currentThreadAccess()) r.run(); else combo.getDisplay().asyncExec(r); } @Override public boolean isDisposed() { return combo.isDisposed(); } }); } if(selectionFactory != null) { selectionFactory.listen(context, input, new Listener() { @Override public void exception(Throwable t) { t.printStackTrace(); } @Override public void execute(final String selectionKey) { if(isDisposed()) return; combo.getDisplay().asyncExec(new Runnable() { @Override public void run() { if(isDisposed()) return; // System.out.println("Combo received new selection key: " + selectionKey); if(selectionKey == null) return; combo.setData("_SelectionKey", selectionKey); Integer selectionIndex = (Integer)combo.getData(selectionKey); if(selectionIndex != null) combo.select(selectionIndex); } }); } @Override public boolean isDisposed() { return combo.isDisposed(); } }); } } public void manualSelect(int index) { String key = combo.getItem(index); combo.setData("_SelectionKey", key); combo.select(index); } 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, SelectionListener { // Keyboard/editing events come in the following order: // 1. keyPressed // 2. verifyText // 3. modifyText // 4. keyReleased @Override 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()); } } @Override public void widgetDisposed(DisposeEvent e) { getWidget().removeModifyListener(this); } @Override 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 (e.keyCode == SWT.F2) { startEdit(true); } else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { startEdit(false); } else if (e.keyCode == SWT.TAB) { combo.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 (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { applyEdit(); e.doit = false; } } } @Override public void keyReleased(KeyEvent e) { //System.out.println("keyReleased: " + e); } @Override public void mouseEnter(MouseEvent e) { //System.out.println("mouseEnter"); if (!isEditing()) { setBackground(colorProvider.getHoverBackground()); } setMouseInsideControl(true); } @Override public void mouseExit(MouseEvent e) { //System.out.println("mouseExit"); if (!isEditing()) { setBackground(colorProvider.getInactiveBackground()); } setMouseInsideControl(false); } @Override public void mouseHover(MouseEvent e) { //System.out.println("mouseHover"); setMouseInsideControl(true); } @Override public void mouseDoubleClick(MouseEvent e) { //System.out.println("mouseDoubleClick: " + e); if (e.button == 1) { getWidget().setSelection(new Point(0, combo.getText().length())); } } @Override 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(true); } } else { if (e.button == 1 && (state & MOUSE_DOWN_FIRST_TIME) != 0) { getWidget().setSelection(new Point(0, combo.getText().length())); state &= ~MOUSE_DOWN_FIRST_TIME; } } } @Override public void mouseUp(MouseEvent e) { } @Override public void focusGained(FocusEvent e) { //System.out.println("focusGained"); if (!isEditing()) { startEdit(true); } } @Override public void focusLost(FocusEvent e) { //System.out.println("focusLost"); if (isEditing()) { applyEdit(); } } @Override public void widgetDefaultSelected(SelectionEvent e) { applyEdit(); } @Override public void widgetSelected(SelectionEvent e) { applyEdit(); } } public TrackedCombo(Composite parent, WidgetSupport support, int style) { combo = new org.eclipse.swt.widgets.Combo(parent, style); combo.setData("org.simantics.browsing.ui.widgets.Combo", this); this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), combo); this.colorProvider = new DefaultColorProvider(); support.register(this); initialize(); } /** * Common initialization. Assumes that text is already created. */ private void initialize() { Assert.isNotNull(combo); combo.setBackground(colorProvider.getInactiveBackground()); // combo.setDoubleClickEnabled(false); listener = new CompositeListener(); combo.addModifyListener(listener); combo.addDisposeListener(listener); combo.addKeyListener(listener); combo.addMouseTrackListener(listener); combo.addMouseListener(listener); combo.addFocusListener(listener); combo.addSelectionListener(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 = combo.getSelection().x; textBeforeEdit = combo.getText(); // Signal editing state setBackground(colorProvider.getEditingBackground()); if (selectAll) { combo.setSelection(new Point(0, combo.getText().length())); } state |= EDITING | MOUSE_DOWN_FIRST_TIME; } private void applyEdit() { try { if (isTextValid() != null) { combo.setText(textBeforeEdit); } else if (isModified() && !combo.getText().equals(textBeforeEdit)) { //System.out.println("apply"); if (modifyListeners != null) { TrackedModifyEvent event = new TrackedModifyEvent(combo, combo.getText()); for (Object o : modifyListeners.getListeners()) { ((TextModifyListener) o).modifyText(event); } } } } finally { endEdit(); } } private void 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 combo.setSelection(new Point(combo.getText().length(), 0)); 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")); System.out.println("BUG: revertEdit called when not in editing state"); } combo.setText(textBeforeEdit); combo.setSelection(new Point(caretPositionBeforeEdit, 0)); setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground()); state &= ~(EDITING | MOUSE_DOWN_FIRST_TIME); setModified(false); } private 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 boolean isModified() { return (state & MODIFIED_DURING_EDITING) != 0; } private void setMouseInsideControl(boolean inside) { if (inside) state |= MOUSE_INSIDE_CONTROL; else state &= ~MOUSE_INSIDE_CONTROL; } public void setEditable(boolean editable) { if (editable) { combo.setEnabled(true); setBackground(isMouseInsideControl() ? colorProvider.getHoverBackground() : colorProvider.getInactiveBackground()); } else { combo.setEnabled(false); combo.setBackground(null); } } public void setText(String text) { this.combo.setText(text); } public void setTextWithoutNotify(String text) { this.combo.removeModifyListener(listener); setText(text); this.combo.addModifyListener(listener); } public org.eclipse.swt.widgets.Combo getWidget() { return combo; } public synchronized void addModifyListener(TextModifyListener listener) { if (modifyListeners == null) { modifyListeners = new ListenerList(ListenerList.IDENTITY); } modifyListeners.add(listener); } public synchronized void removeModifyListener(TextModifyListener listener) { if (modifyListeners == null) return; modifyListeners.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; } public void setBackground(Color color) { if (!combo.getEnabled()) { // Do not alter background when the widget is not editable. return; } combo.setBackground(color); } public void setForeground(Color color) { combo.setForeground(color); } public boolean isDisposed() { return combo.isDisposed(); } public Display getDisplay() { return combo.getDisplay(); } }